[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# All files\n[*]\nindent_style = space\n\n# Xml files\n[*.xml]\nindent_size = 2\n\n# C# files\n[*.cs]\n\n#### Core EditorConfig Options ####\n\n# Indentation and spacing\nindent_size = 4\nindent_style = space\ntab_width = 4\n\n# New line preferences\nend_of_line = crlf\ninsert_final_newline = false\n\n#### .NET Coding Conventions ####\n[*.{cs,vb}]\n\n# Organize usings\ndotnet_separate_import_directive_groups = true\ndotnet_sort_system_directives_first = true\nfile_header_template = unset\n\n# this. and Me. preferences\ndotnet_style_qualification_for_event = false:silent\ndotnet_style_qualification_for_field = false:silent\ndotnet_style_qualification_for_method = false:silent\ndotnet_style_qualification_for_property = false:silent\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true:silent\ndotnet_style_predefined_type_for_member_access = true:silent\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members:silent\n\n# Expression-level preferences\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\ndotnet_style_prefer_auto_properties = true:suggestion\ndotnet_style_prefer_compound_assignment = true:suggestion\ndotnet_style_prefer_conditional_expression_over_assignment = true:suggestion\ndotnet_style_prefer_conditional_expression_over_return = true:suggestion\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\ndotnet_style_prefer_inferred_tuple_names = true:suggestion\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion\ndotnet_style_prefer_simplified_boolean_expressions = true:suggestion\ndotnet_style_prefer_simplified_interpolation = true:suggestion\n\n# Field preferences\ndotnet_style_readonly_field = true:warning\n\n# Parameter preferences\ndotnet_code_quality_unused_parameters = all:suggestion\n\n# Suppression preferences\ndotnet_remove_unnecessary_suppression_exclusions = none\n\n# CA1806: Do not ignore method results\ndotnet_diagnostic.CA1806.severity = error\n\n#### C# Coding Conventions ####\n[*.cs]\n\n# var preferences\ncsharp_style_var_elsewhere = false:silent\ncsharp_style_var_for_built_in_types = false:silent\ncsharp_style_var_when_type_is_apparent = false:silent\n\n# Expression-bodied members\ncsharp_style_expression_bodied_accessors = true:silent\ncsharp_style_expression_bodied_constructors = false:silent\ncsharp_style_expression_bodied_indexers = true:silent\ncsharp_style_expression_bodied_lambdas = true:suggestion\ncsharp_style_expression_bodied_local_functions = false:silent\ncsharp_style_expression_bodied_methods = false:silent\ncsharp_style_expression_bodied_operators = false:silent\ncsharp_style_expression_bodied_properties = true:silent\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_prefer_not_pattern = true:suggestion\ncsharp_style_prefer_pattern_matching = true:silent\ncsharp_style_prefer_switch_expression = true:suggestion\n\n# Null-checking preferences\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Modifier preferences\ncsharp_prefer_static_local_function = true:warning\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent\n\n# Code-block preferences\ncsharp_prefer_braces = true:silent\ncsharp_prefer_simple_using_statement = true:suggestion\n\n# Expression-level preferences\ncsharp_prefer_simple_default_expression = true:suggestion\ncsharp_style_deconstructed_variable_declaration = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_pattern_local_over_anonymous_function = true:suggestion\ncsharp_style_prefer_index_operator = true:suggestion\ncsharp_style_prefer_range_operator = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_unused_value_assignment_preference = discard_variable:suggestion\ncsharp_style_unused_value_expression_statement_preference = discard_variable:silent\n\n# 'using' directive preferences\ncsharp_using_directive_placement = outside_namespace:silent\n\n#### C# Formatting Rules ####\n\n# New line preferences\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_else = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation preferences\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = true\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = true\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_before_comma = false\ncsharp_space_before_dot = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_between_square_brackets = false\n\n# Wrapping preferences\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\n\n#### Naming styles ####\n[*.{cs,vb}]\n\n# Naming rules\n\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion\ndotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces\ndotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase\n\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase\n\ndotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.methods_should_be_pascalcase.symbols = methods\ndotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.properties_should_be_pascalcase.symbols = properties\ndotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.events_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.events_should_be_pascalcase.symbols = events\ndotnet_naming_rule.events_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables\ndotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants\ndotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters\ndotnet_naming_rule.parameters_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields\ndotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion\ndotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields\ndotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase\n\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase\n\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.enums_should_be_pascalcase.symbols = enums\ndotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions\ndotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members\ndotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase\n\n# Symbol specifications\n\ndotnet_naming_symbols.interfaces.applicable_kinds = interface\ndotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.interfaces.required_modifiers = \n\ndotnet_naming_symbols.enums.applicable_kinds = enum\ndotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.enums.required_modifiers = \n\ndotnet_naming_symbols.events.applicable_kinds = event\ndotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.events.required_modifiers = \n\ndotnet_naming_symbols.methods.applicable_kinds = method\ndotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.methods.required_modifiers = \n\ndotnet_naming_symbols.properties.applicable_kinds = property\ndotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.properties.required_modifiers = \n\ndotnet_naming_symbols.public_fields.applicable_kinds = field\ndotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_fields.required_modifiers = \n\ndotnet_naming_symbols.private_fields.applicable_kinds = field\ndotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_fields.required_modifiers = \n\ndotnet_naming_symbols.private_static_fields.applicable_kinds = field\ndotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_static_fields.required_modifiers = static\n\ndotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum\ndotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.types_and_namespaces.required_modifiers = \n\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.non_field_members.required_modifiers = \n\ndotnet_naming_symbols.type_parameters.applicable_kinds = namespace\ndotnet_naming_symbols.type_parameters.applicable_accessibilities = *\ndotnet_naming_symbols.type_parameters.required_modifiers = \n\ndotnet_naming_symbols.private_constant_fields.applicable_kinds = field\ndotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_constant_fields.required_modifiers = const\n\ndotnet_naming_symbols.local_variables.applicable_kinds = local\ndotnet_naming_symbols.local_variables.applicable_accessibilities = local\ndotnet_naming_symbols.local_variables.required_modifiers = \n\ndotnet_naming_symbols.local_constants.applicable_kinds = local\ndotnet_naming_symbols.local_constants.applicable_accessibilities = local\ndotnet_naming_symbols.local_constants.required_modifiers = const\n\ndotnet_naming_symbols.parameters.applicable_kinds = parameter\ndotnet_naming_symbols.parameters.applicable_accessibilities = *\ndotnet_naming_symbols.parameters.required_modifiers = \n\ndotnet_naming_symbols.public_constant_fields.applicable_kinds = field\ndotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_constant_fields.required_modifiers = const\n\ndotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field\ndotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static\n\ndotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field\ndotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static\n\ndotnet_naming_symbols.local_functions.applicable_kinds = local_function\ndotnet_naming_symbols.local_functions.applicable_accessibilities = *\ndotnet_naming_symbols.local_functions.required_modifiers = \n\n# Naming styles\n\ndotnet_naming_style.pascalcase.required_prefix = \ndotnet_naming_style.pascalcase.required_suffix = \ndotnet_naming_style.pascalcase.word_separator = \ndotnet_naming_style.pascalcase.capitalization = pascal_case\n\ndotnet_naming_style.ipascalcase.required_prefix = I\ndotnet_naming_style.ipascalcase.required_suffix = \ndotnet_naming_style.ipascalcase.word_separator = \ndotnet_naming_style.ipascalcase.capitalization = pascal_case\n\ndotnet_naming_style.tpascalcase.required_prefix = T\ndotnet_naming_style.tpascalcase.required_suffix = \ndotnet_naming_style.tpascalcase.word_separator = \ndotnet_naming_style.tpascalcase.capitalization = pascal_case\n\ndotnet_naming_style._camelcase.required_prefix = _\ndotnet_naming_style._camelcase.required_suffix = \ndotnet_naming_style._camelcase.word_separator = \ndotnet_naming_style._camelcase.capitalization = camel_case\n\ndotnet_naming_style.camelcase.required_prefix = \ndotnet_naming_style.camelcase.required_suffix = \ndotnet_naming_style.camelcase.word_separator = \ndotnet_naming_style.camelcase.capitalization = camel_case\n\ndotnet_naming_style.s_camelcase.required_prefix = s_\ndotnet_naming_style.s_camelcase.required_suffix = \ndotnet_naming_style.s_camelcase.word_separator = \ndotnet_naming_style.s_camelcase.capitalization = camel_case\n\n"
  },
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n*.ps1 eol=lf\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: cncnet\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: Open an issue to ask for a XNA Client bug to be fixed.\ntitle: \"Your bug report title here\"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        > [!WARNING]\n        > Before posting an issue, please read the **checklist at the bottom**.\n\n        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.\n\n        Please make sure you follow these instructions and fill in every question with as much detail as possible.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: |\n        Write a detailed description telling us what the issue is, and if/when the bug occurs.\n    validations:\n      required: true\n\n  - type: input\n    id: xna-client-version\n    attributes:\n      label: XNA Client Version\n      description: |\n        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.\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps To Reproduce\n      description: |\n        Tell us how to reproduce this issue so the developer(s) can reproduce the bug.\n      value: |\n        1.\n        2.\n        3.\n        ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behaviour\n      description: |\n        Tell us what should happen.\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual Behaviour\n      description: |\n        Tell us what actually happens instead.\n    validations:\n      required: true\n\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: |\n        Attach additional files or links to content related to the bug report here, like:\n        - images/gifs/videos to illustrate the bug;\n        - files or ini configs that are needed to reproduce the bug;\n        - a client log (mandatory if you're submitting a crash report).\n\n  - type: checkboxes\n    id: checks\n    attributes:\n      label: Checklist\n      description: Please read and ensure you followed all the following options.\n      options:\n        - label: The issue happens on the **latest official** version of XNA Client and wasn't fixed yet.\n          required: true\n        - label: I agree to elaborate the details if requested and provide thorough testing if the bugfix is implemented.\n          required: true\n        - label: I added a very descriptive title to this issue.\n          required: true\n        - label: I used the GitHub search and read the issue list to find a similar issue and didn't find it.\n          required: true\n        - label: I have attached as much information as possible *(screenshots, gifs, videos, client logs, etc)*.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Official channels on C&C Mod Haven\n    url: https://discord.gg/Smv4JC8BUG\n    about: If you want to discuss something with us without filing an issue.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: Feature Request\ndescription: Open an issue to ask for a XNA Client feature to be implemented.\ntitle: \"Your feature request title here\"\nlabels: [\"feature\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        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.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: |\n        Write a detailed description telling us what the feature you want to be implemented in the XNA Client.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/copilot-coding-agent-setup.md",
    "content": "# GitHub Copilot coding agent setup instructions\n\nThis 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.\n\nThe 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.\n\n## Step 1 — Initialize git submodules\n\n`Rampastring.XNAUI` (and its nested submodule `Rampastring.Tools`) may not be pre-initialized. Missing them causes compile errors about unknown `Rampastring.*` types.\n\n```shell\ngit submodule update --init --recursive\n```\n\n## Step 2 — Unshallow the clone and fetch `develop`\n\nThe build system uses **GitVersion.MsBuild** to compute version numbers at compile time. It requires two things:\n\n- A full (non-shallow) commit history.\n- 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`.\n\nRun all three commands unconditionally:\n\n- `--unshallow` is a no-op on an already-full clone (`|| true` prevents it from aborting).\n- `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.\n- The final fetch brings `refs/remotes/origin/develop` into the local ref store through that glob refspec so GitVersion can find it.\n\nThe 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.\n\n```shell\ngit fetch --unshallow origin || true\ngit remote set-branches origin '*'\ngit fetch origin develop\ngit submodule foreach --recursive \\\n  'git fetch --unshallow origin || true; git remote set-branches origin \"*\"; git fetch origin'\n```\n\n## Step 3 — Restore NuGet packages\n\nRun 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.\n\n```shell\ndotnet restore -p:Configuration=UniversalGLRelease\n```\n\n## Step 4 — Build\n\n```shell\ndotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore\n```\n\nA successful build ends with `0 Error(s)`.\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Agent Instructions\n\n\n## General information\n\n### Project structure\n\n| Path | Description |\n|------|-------------|\n| `DXMainClient/` | Main entry-point project — always the build target |\n| `ClientCore/` | Core game-client logic |\n| `ClientGUI/` | UI layer |\n| `ClientUpdater/` | Auto-updater logic |\n| `SecondStageUpdater/` | Secondary updater executable |\n| `Rampastring.XNAUI/` | UI framework (git submodule) |\n| `GitVersion.yml` | GitVersion branch and versioning strategy |\n| `global.json` | Pins the required .NET SDK version (10.0, any feature band) |\n| `Directory.Build.props` | MSBuild properties shared across all projects |\n| `Directory.Packages.props` | Central NuGet package version management |\n| `Docs/Build.md` | Human-oriented build documentation |\n\n### Build the project\n\n```shell\ndotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0\n```\n\nA successful build ends with `0 Error(s)`.\n\n### Contributing guidelines\nSee [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.\n\n## GitHub Copilot coding agent setup instructions\n\nThis 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.\n\nThe 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**."
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build client\n\non:\n  push:\n    branches: [ master, develop ]\n  pull_request:\n    branches: [ master, develop ]\n  workflow_dispatch:\njobs:\n  build-clients:\n    runs-on: windows-2022\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: recursive\n\n    - name: Setup .NET SDK\n      uses: actions/setup-dotnet@v5\n      with:\n        global-json-file: ./global.json\n\n    - name: Build\n      run: ./Scripts/build.ps1\n      shell: pwsh\n\n    - uses: actions/upload-artifact@v4\n      name: Upload Artifacts\n      with:\n        name: artifacts\n        path: ./Compiled\n"
  },
  {
    "path": ".github/workflows/copilot-setup-steps.yml",
    "content": "name: Copilot setup steps\n\n# Automatically run the setup steps when they are changed to allow for easy validation, and\n# allow manual testing through the repository's \"Actions\" tab\non:\n  workflow_dispatch:\n  push:\n    paths:\n      - .github/workflows/copilot-setup-steps.yml\n  pull_request:\n    paths:\n      - .github/workflows/copilot-setup-steps.yml\n\njobs:\n  # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.\n  copilot-setup-steps:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout code with full history and submodules\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: recursive\n      \n      # 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\n      - name: Unshallow clone and fetch develop branch\n        run: |\n          git fetch --unshallow origin || true\n          git remote set-branches origin '*'\n          git fetch origin develop\n          # Apply the same fix to every submodule (including nested ones), because GitVersion.MsBuild\n          # runs against each submodule directory that carries it, and the same LibGit2Sharp refspec\n          # crash occurs there when the submodule was checked out with a narrow single-branch refspec.\n          git submodule foreach --recursive \\\n            'git fetch --unshallow origin || true; git remote set-branches origin \"*\"; git fetch origin'\n\n      - name: Set up .NET SDK\n        uses: actions/setup-dotnet@v4\n        with:\n          global-json-file: ./global.json\n\n      - name: Restore NuGet packages\n        run: dotnet restore -p:Configuration=UniversalGLRelease\n\n      - name: Build\n        run: dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore\n"
  },
  {
    "path": ".github/workflows/pr-build-comment.yml",
    "content": "name: automatic comment on pull request\non:\n  workflow_run:\n    workflows: ['build client']\n    types: [completed]\njobs:\n  pr_comment:\n    if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/github-script@v6\n        with:\n          # This snippet is public-domain, taken from\n          # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml\n          script: |\n            const {owner, repo} = context.repo;\n            const run_id = ${{github.event.workflow_run.id}};\n            const pull_head_sha = '${{github.event.workflow_run.head_sha}}';\n            const pull_user_id = ${{github.event.sender.id}};\n            const issue_number = await (async () => {\n              const pulls = await github.rest.pulls.list({owner, repo});\n              for await (const {data} of github.paginate.iterator(pulls)) {\n                for (const pull of data) {\n                  if (pull.head.sha === pull_head_sha && pull.user.id === pull_user_id) {\n                    return pull.number;\n                  }\n                }\n              }\n            })();\n            if (issue_number) {\n              core.info(`Using pull request ${issue_number}`);\n            } else {\n              return core.error(`No matching pull request found`);\n            }\n            const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id});\n            if (!artifacts.length) {\n              return core.error(`No artifacts found`);\n            }\n            let body = `Nightly build for this pull request:\\n`;\n            for (const art of artifacts) {\n              body += `\\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;\n            }\n            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.`;\n            const {data: comments} = await github.rest.issues.listComments({repo, owner, issue_number});\n            const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]');\n            if (existing_comment) {\n              core.info(`Updating comment ${existing_comment.id}`);\n              await github.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body});\n            } else {\n              core.info(`Creating a comment`);\n              await github.rest.issues.createComment({repo, owner, issue_number, body});\n            }\n"
  },
  {
    "path": ".github/workflows/release-build.yml",
    "content": "name: release build\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: write\n\njobs:\n  build:\n    runs-on: windows-2022\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: recursive\n\n    - name: Setup .NET SDK\n      uses: actions/setup-dotnet@v5\n      with:\n        global-json-file: ./global.json\n\n    - name: Install GitVersion\n      uses: gittools/actions/gitversion/setup@v0\n      with:\n        versionSpec: '5.x'\n\n    - name: Determine Version\n      uses: gittools/actions/gitversion/execute@v0\n\n    - name: Development Build Check\n      if: \"!github.event.release.prerelease\"\n      shell: pwsh\n      run: |\n        if ($env:GitVersion_CommitsSinceVersionSource -ne \"0\") {\n          Write-Output \"::error:: This is a development build and should not be released. Did you forget to create a new tag for the release?\"\n          exit 1\n        }\n\n    - name: Build\n      run: ./Scripts/build.ps1\n      shell: pwsh\n\n    - name: Zip Artifact\n      run: 7z a -t7z -mx=9 -m0=lzma2 -ms=on -r -- ${{ format('xna-cncnet-client-{0}.7z', env.GitVersion_SemVer) }} ./Compiled/*\n      shell: pwsh\n\n    - name: Upload Final Artifact to the Release\n      uses: softprops/action-gh-release@v2\n      with:\n        append_body: true\n        files: ${{ format('xna-cncnet-client-{0}.7z', env.GitVersion_SemVer) }}\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n\n# ... with an exception in References folder\n!**/[Rr]eferences/*.nupkg\n!**/[Rr]eferences/*.snupkg\n\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n*.ncb\n*.aps\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n\n# CnCNet\nCompiled/\nCompiled*/\n\n# Käyttiksen (Mac ja win) tekemiä tiedostoja joita jättää pois\n._*\n.DS_Store*\n.Spotlight-V100\n.Trashes\nehthumbs.db\nIcon?\n[Tt]humbs.db\n\n# Dolphin\n.directory\n\n.idea/\n\n# Game specific build prop files\nDirectory.Build.Game.*.props\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"Rampastring.XNAUI\"]\n\tpath = Rampastring.XNAUI\n\turl = https://github.com/CnCNet/Rampastring.XNAUI.git\n"
  },
  {
    "path": "AdditionalFiles/UpdateServerScripts/preupdateexec",
    "content": "[Rename]\nOLD_FILE_PATH=NEW_FILE_PATH\n\n[Delete]\nFILE_PATH\n\n[RenameFolder]\nOLD_FOLDER_PATH=NEW_FOLDER_PATH\n\n[RenameAndMerge]\nOLD_FOLDER_PATH=NEW_FOLDER_PATH\n\n[ForceDeleteFolder]\nFOLDER_PATH\n\n[DeleteFolderIfEmpty]\nFOLDER_PATH\n\n"
  },
  {
    "path": "AdditionalFiles/UpdateServerScripts/updateexec",
    "content": "[Rename]\nOLD_FILE_PATH=NEW_FILE_PATH\n\n[Delete]\nFILE_PATH\n\n[RenameFolder]\nOLD_FOLDER_PATH=NEW_FOLDER_PATH\n\n[RenameAndMerge]\nOLD_FOLDER_PATH=NEW_FOLDER_PATH\n\n[ForceDeleteFolder]\nFOLDER_PATH\n\n[DeleteFolderIfEmpty]\nFOLDER_PATH\n\n"
  },
  {
    "path": "AdditionalFiles/VersionFileWriter/VersionConfig.ini",
    "content": "; Mod version.\n[Version]\n1\n\n; Mod updater version.\n; 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.\n; Omit or set to N/A if not wishing to use this feature.\n[UpdaterVersion]\nN/A\n\n; If set client will show a dialog prompting for manual download with the provided link if a manual update download is required.\n; Omit if wishing to not use this feature.\n[ManualDownloadURL]\n\n[Options]\n; If set, enables the extended updater features such as archives, updater version and manual download URL.\nEnableExtendedUpdaterFeatures=yes\n; If set, will go through every subdirectory recursively for directories given in Include.\nRecursiveDirectorySearch=yes\n; 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). \n; version_base should be kept around as it is used to compare which files have been changed next time VersionWriter is ran.\nIncludeOnlyChangedFiles=no\n; If set, original versions of archived files will also be copied to copied files directory.\nCopyArchivedOriginalFiles=no\n; 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.\nExcludeHiddenAndSystemFiles=yes\n; If set, the mod version string is treated as .NET timestamp/datetime format string with current local time applied on it.\nApplyTimestampOnVersion=no\n; 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.\nNoCopyMode=no\n\n; Files & directories to include in version file.\n[Include]\ntest.file\ntest2.file\nTest\\\n\n; Files (not directories) to be excluded from included files list.\n; User-generated (settings etc), temporary and log files should be listed here.\n[ExcludeFiles]\nTest\\test2.file\n\n; Directories to be excluded from included files list\n; If you include entire directory trees f.ex map editors, this is useful to exclude things like autosave or log directories.\n[ExcludeDirectories]\nTest\\TestDir\n\n; Files (not directories) to be included as archives.\n[ArchiveFiles]\ntest.file\n\n; Custom components. ID's and filenames are normally hardcoded, but also overridable through UpdaterConfig.ini.\n[AddOns]\nCOMPONENT_ID=customcomp.mix\n"
  },
  {
    "path": "ClientCore/CCIniFile.cs",
    "content": "﻿using Rampastring.Tools;\nusing System.IO;\n\nnamespace ClientCore\n{\n    public class CCIniFile : IniFile\n    {\n        public CCIniFile(string path) : base(path)\n        {\n            foreach (IniSection section in Sections)\n            {\n                string baseSectionName = section.GetStringValue(\"$BaseSection\", null);\n\n                if (string.IsNullOrWhiteSpace(baseSectionName))\n                    continue;\n\n                var baseSection = Sections.Find(s => s.SectionName == baseSectionName);\n                if (baseSection == null)\n                {\n                    Logger.Log($\"Base section not found in INI file {path}, section {section.SectionName}, base section name: {baseSectionName}\");\n                    continue;\n                }\n\n                int addedKeyCount = 0;\n\n                foreach (var kvp in baseSection.Keys)\n                {\n                    if (!section.KeyExists(kvp.Key))\n                    {\n                        section.Keys.Insert(addedKeyCount, kvp);\n                        addedKeyCount++;\n                    }\n                }\n            }\n        }\n\n        protected override void ApplyBaseIni()\n        {\n            string basedOnSetting = GetStringValue(\"INISystem\", \"BasedOn\", string.Empty);\n            if (string.IsNullOrEmpty(basedOnSetting))\n                return;\n\n            string[] basedOns = basedOnSetting.Split(',');\n            foreach (string basedOn in basedOns)\n                ApplyBasedOnIni(basedOn);\n        }\n\n        private void ApplyBasedOnIni(string basedOn)\n        {\n            if (string.IsNullOrEmpty(basedOn))\n                return;\n\n            FileInfo baseIniFile;\n            if (basedOn.Contains(\"$THEME_DIR$\"))\n                baseIniFile = SafePath.GetFile(basedOn.Replace(\"$THEME_DIR$\", ProgramConstants.GetResourcePath()));\n            else\n                baseIniFile = SafePath.GetFile(SafePath.GetFileDirectoryName(FileName), basedOn);\n\n            // Consolidate with the INI file that this INI file is based on\n            if (!baseIniFile.Exists)\n                Logger.Log(FileName + \": Base INI file not found! \" + baseIniFile.FullName);\n\n            CCIniFile baseIni = new CCIniFile(baseIniFile.FullName);\n            ConsolidateIniFiles(baseIni, this);\n            Sections = baseIni.Sections;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/ClientConfiguration.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropServices;\n\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\nusing ClientCore.I18N;\n\nusing Rampastring.Tools;\n\nnamespace ClientCore\n{\n    public class ClientConfiguration\n    {\n        private const string GENERAL = \"General\";\n        private const string AUDIO = \"Audio\";\n        private const string SETTINGS = \"Settings\";\n        private const string LINKS = \"Links\";\n        private const string TRANSLATIONS = \"Translations\";\n        private const string USER_DEFAULTS = \"UserDefaults\";\n\n        public const string CLIENT_SETTINGS = \"DTACnCNetClient.ini\";\n        public const string GAME_OPTIONS = \"GameOptions.ini\";\n        public const string CLIENT_DEFS = \"ClientDefinitions.ini\";\n        public const string NETWORK_DEFS_LOCAL = \"NetworkDefinitions.local.ini\";\n        public const string NETWORK_DEFS = \"NetworkDefinitions.ini\";\n\n        private static ClientConfiguration _instance;\n\n        private IniFile gameOptions_ini;\n        private IniFile DTACnCNetClient_ini;\n        private IniFile clientDefinitionsIni;\n        private IniFile networkDefinitionsIni;\n\n        protected ClientConfiguration()\n        {\n            var baseResourceDirectory = SafePath.GetDirectory(ProgramConstants.GetBaseResourcePath());\n\n            if (!baseResourceDirectory.Exists)\n                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.\");\n\n            FileInfo clientDefinitionsFile = SafePath.GetFile(baseResourceDirectory.FullName, CLIENT_DEFS);\n\n            if (!(clientDefinitionsFile?.Exists ?? false))\n                throw new FileNotFoundException($\"Couldn't find {CLIENT_DEFS} at {baseResourceDirectory}. Please verify that you're running the client from the correct directory.\");\n\n            clientDefinitionsIni = new IniFile(clientDefinitionsFile.FullName);\n\n            DTACnCNetClient_ini = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), CLIENT_SETTINGS));\n\n            gameOptions_ini = new IniFile(SafePath.CombineFilePath(baseResourceDirectory.FullName, GAME_OPTIONS));\n\n            string networkDefsPathLocal = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), NETWORK_DEFS_LOCAL);\n            if (File.Exists(networkDefsPathLocal))\n            {\n                networkDefinitionsIni = new IniFile(networkDefsPathLocal);\n                Logger.Log(\"Loaded network definitions from NetworkDefinitions.local.ini (user override)\");\n            }\n            else\n            {\n                string networkDefsPath = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), NETWORK_DEFS);\n                networkDefinitionsIni = new IniFile(networkDefsPath);\n            }\n\n            RefreshTranslationGameFiles();\n        }\n\n        /// <summary>\n        /// Singleton Pattern. Returns the object of this class.\n        /// </summary>\n        /// <returns>The object of the ClientConfiguration class.</returns>\n        public static ClientConfiguration Instance\n        {\n            get\n            {\n                if (_instance == null)\n                {\n                    _instance = new ClientConfiguration();\n                }\n                return _instance;\n            }\n        }\n\n        public void RefreshSettings()\n        {\n            DTACnCNetClient_ini = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), CLIENT_SETTINGS));\n        }\n\n        #region Client settings\n\n        private string _mainMenuMusicName = null;\n        public string MainMenuMusicName => _mainMenuMusicName ??= GetMainMenuMusicName();\n        private string GetMainMenuMusicName()\n        {\n            string raw = DTACnCNetClient_ini.GetStringValue(GENERAL, \"MainMenuTheme\", \"mainmenu\");\n            string[] parts = raw.SplitWithCleanup();\n            string chosen = parts.Length > 0\n                ? parts[new Random().Next(parts.Length)]\n                : \"mainmenu\";\n\n            return SafePath.CombineFilePath(chosen);\n        }\n\n        public float DefaultAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, \"AlphaRate\", 0.005f);\n\n        public float CheckBoxAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, \"CheckBoxAlphaRate\", 0.05f);\n\n        public float IndicatorAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, \"IndicatorAlphaRate\", 0.05f);\n\n        #region Color settings\n\n        public string UILabelColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"UILabelColor\", \"0,0,0\");\n\n        public string UIHintTextColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"HintTextColor\", \"128,128,128\");\n\n        public string DisabledButtonColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"DisabledButtonColor\", \"108,108,108\");\n\n        public string AltUIColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"AltUIColor\", \"255,255,255\");\n\n        public string ButtonHoverColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"ButtonHoverColor\", \"255,192,192\");\n\n        public string MapPreviewNameBackgroundColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"MapPreviewNameBackgroundColor\", \"0,0,0,144\");\n\n        public string MapPreviewNameBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"MapPreviewNameBorderColor\", \"128,128,128,128\");\n\n        public string MapPreviewStartingLocationHoverRemapColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"StartingLocationHoverColor\", \"255,255,255,128\");\n\n        public bool MapPreviewStartingLocationUsePlayerRemapColor => DTACnCNetClient_ini.GetBooleanValue(GENERAL, \"StartingLocationsUsePlayerRemapColor\", false);\n\n        public string AltUIBackgroundColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"AltUIBackgroundColor\", \"196,196,196\");\n\n        public string WindowBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"WindowBorderColor\", \"128,128,128\");\n\n        public string PanelBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"PanelBorderColor\", \"255,255,255\");\n\n        public string ListBoxHeaderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"ListBoxHeaderColor\", \"255,255,255\");\n\n        public string DefaultChatColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"DefaultChatColor\", \"0,255,0\");\n\n        public string AdminNameColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"AdminNameColor\", \"255,0,0\");\n\n        public string ReceivedPMColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"PrivateMessageOtherUserColor\", \"196,196,196\");\n\n        public string SentPMColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"PrivateMessageColor\", \"128,128,128\");\n\n        public int DefaultPersonalChatColorIndex => DTACnCNetClient_ini.GetIntValue(GENERAL, \"DefaultPersonalChatColorIndex\", 0);\n\n        public string ListBoxFocusColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"ListBoxFocusColor\", \"64,64,168\");\n\n        public string HoverOnGameColor => DTACnCNetClient_ini.GetStringValue(GENERAL, \"HoverOnGameColor\", \"32,32,84\");\n\n        public IniSection GetParserConstants() => DTACnCNetClient_ini.GetSection(\"ParserConstants\");\n\n        #endregion\n\n        #region Tool tip settings\n\n        public int ToolTipFontIndex => DTACnCNetClient_ini.GetIntValue(GENERAL, \"ToolTipFontIndex\", 0);\n\n        public int ToolTipOffsetX => DTACnCNetClient_ini.GetIntValue(GENERAL, \"ToolTipOffsetX\", 0);\n\n        public int ToolTipOffsetY => DTACnCNetClient_ini.GetIntValue(GENERAL, \"ToolTipOffsetY\", 0);\n\n        public int ToolTipMargin => DTACnCNetClient_ini.GetIntValue(GENERAL, \"ToolTipMargin\", 4);\n\n        public float ToolTipDelay => DTACnCNetClient_ini.GetSingleValue(GENERAL, \"ToolTipDelay\", 0.67f);\n\n        public float ToolTipAlphaRatePerSecond => DTACnCNetClient_ini.GetSingleValue(GENERAL, \"ToolTipAlphaRate\", 4.0f);\n\n        #endregion\n\n        #region Audio options\n\n        public float SoundGameLobbyJoinCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundGameLobbyJoinCooldown\", 0.25f);\n\n        public float SoundGameLobbyLeaveCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundGameLobbyLeaveCooldown\", 0.25f);\n\n        public float SoundMessageCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundMessageCooldown\", 0.25f);\n\n        public float SoundPrivateMessageCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundPrivateMessageCooldown\", 0.25f);\n\n        public float SoundGameLobbyGetReadyCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundGameLobbyGetReadyCooldown\", 5.0f);\n\n        public float SoundGameLobbyReturnCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, \"SoundGameLobbyReturnCooldown\", 1.0f);\n\n        #endregion\n\n        #endregion\n\n        #region Game options\n\n        public string Sides => gameOptions_ini.GetStringValue(GENERAL, nameof(Sides), \"GDI,Nod,Allies,Soviet\");\n\n        public string InternalSideIndices => gameOptions_ini.GetStringValue(GENERAL, nameof(InternalSideIndices), string.Empty);\n\n        public string SpectatorInternalSideIndex => gameOptions_ini.GetStringValue(GENERAL, nameof(SpectatorInternalSideIndex), string.Empty);\n\n        #endregion\n\n        #region Client definitions\n\n        private string _ClientGameTypeString => clientDefinitionsIni.GetStringValue(SETTINGS, \"ClientGameType\", string.Empty);\n        private ClientType? _ClientGameType = null;\n        public ClientType ClientGameType => _ClientGameType ??= ClientTypeHelper.FromString(_ClientGameTypeString);\n\n        public string DiscordAppId => clientDefinitionsIni.GetStringValue(SETTINGS, \"DiscordAppId\", string.Empty);\n\n        public int SendSleep => clientDefinitionsIni.GetIntValue(SETTINGS, \"SendSleep\", 2500);\n\n        public int LoadingScreenCount => clientDefinitionsIni.GetIntValue(SETTINGS, \"LoadingScreenCount\", 2);\n\n        public int ThemeCount => clientDefinitionsIni.GetSectionKeys(\"Themes\").Count;\n\n        public string LocalGame => clientDefinitionsIni.GetStringValue(SETTINGS, \"LocalGame\", \"DTA\");\n\n        public bool SidebarHack => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"SidebarHack\", false);\n\n        public int MinimumRenderWidth => clientDefinitionsIni.GetIntValue(SETTINGS, \"MinimumRenderWidth\", 1280);\n\n        public int MinimumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, \"MinimumRenderHeight\", 768);\n\n        public int MaximumRenderWidth => clientDefinitionsIni.GetIntValue(SETTINGS, \"MaximumRenderWidth\", 1280);\n\n        public int MaximumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, \"MaximumRenderHeight\", 800);\n\n        public string[] RecommendedResolutions => clientDefinitionsIni.GetStringListValue(SETTINGS, \"RecommendedResolutions\",\n            $\"{MinimumRenderWidth}x{MinimumRenderHeight},{MaximumRenderWidth}x{MaximumRenderHeight}\");\n\n        public string WindowTitle => clientDefinitionsIni.GetStringValue(SETTINGS, \"WindowTitle\", string.Empty)\n            .L10N(\"INI:ClientDefinitions:WindowTitle\");\n\n        public string InstallationPathRegKey => clientDefinitionsIni.GetStringValue(SETTINGS, \"RegistryInstallPath\", \"TiberianSun\");\n\n        public string CnCNetLiveStatusIdentifier => clientDefinitionsIni.GetStringValue(SETTINGS, \"CnCNetLiveStatusIdentifier\", \"cncnet5_ts\");\n\n        public string BattleFSFileName => clientDefinitionsIni.GetStringValue(SETTINGS, \"BattleFSFileName\", \"BattleFS.ini\");\n\n        public string MapEditorExePath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, \"MapEditorExePath\", SafePath.CombineFilePath(\"FinalSun\", \"FinalSun.exe\")));\n\n        public string UnixMapEditorExePath => clientDefinitionsIni.GetStringValue(SETTINGS, \"UnixMapEditorExePath\", Instance.MapEditorExePath);\n\n        public bool ModMode => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"ModMode\", false);\n\n        public string LongGameName => clientDefinitionsIni.GetStringValue(SETTINGS, \"LongGameName\", \"Tiberian Sun\");\n\n        public string LongSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, \"LongSupportURL\", \"https://www.moddb.com/members/rampastring\");\n\n        public string ShortSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, \"ShortSupportURL\", \"www.moddb.com/members/rampastring\");\n\n        public string ChangelogURL => clientDefinitionsIni.GetStringValue(SETTINGS, \"ChangelogURL\", \"https://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/change-log\");\n\n        public string CreditsURL => clientDefinitionsIni.GetStringValue(SETTINGS, \"CreditsURL\", \"https://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/credits#Rampastring\");\n\n        public string ManualDownloadURL => clientDefinitionsIni.GetStringValue(SETTINGS, \"ManualDownloadURL\", string.Empty);\n\n        public string FinalSunIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, \"FSIniPath\", SafePath.CombineFilePath(\"FinalSun\", \"FinalSun.ini\")));\n\n        public int MaxNameLength => clientDefinitionsIni.GetIntValue(SETTINGS, \"MaxNameLength\", 16);\n\n        public bool UseIsometricCells => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"UseIsometricCells\", true);\n\n        public int WaypointCoefficient => clientDefinitionsIni.GetIntValue(SETTINGS, \"WaypointCoefficient\", 128);\n\n        public int MapCellSizeX => clientDefinitionsIni.GetIntValue(SETTINGS, \"MapCellSizeX\", 48);\n\n        public int MapCellSizeY => clientDefinitionsIni.GetIntValue(SETTINGS, \"MapCellSizeY\", 24);\n\n        public bool UseBuiltStatistic => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"UseBuiltStatistic\", false);\n\n        public string WindowedModeKey => clientDefinitionsIni.GetStringValue(SETTINGS, \"WindowedModeKey\", \"Video.Windowed\");\n\n        public bool CopyResolutionDependentLanguageDLL => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"CopyResolutionDependentLanguageDLL\", true);\n\n        public string StatisticsLogFileName => clientDefinitionsIni.GetStringValue(SETTINGS, \"StatisticsLogFileName\", \"DTA.LOG\");\n\n        public string[] TrustedDomains => clientDefinitionsIni.GetStringListValue(SETTINGS, \"TrustedDomains\", string.Empty);\n\n        public string[] AlwaysTrustedDomains = { \"cncnet.org\", \"gamesurge.net\", \"dronebl.org\", \"discord.com\", \"discord.gg\", \"youtube.com\", \"youtu.be\" };\n\n        public bool ShowGameIconInGameList => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"ShowGameIconInGameList\", true);\n\n        public (string Name, string Path) GetThemeInfoFromIndex(int themeIndex) => clientDefinitionsIni.GetStringValue(\"Themes\", themeIndex.ToString(), \",\").Split(',').AsTuple2();\n\n        /// <summary>\n        /// Returns the directory path for a theme, or null if the specified\n        /// theme name doesn't exist.\n        /// </summary>\n        /// <param name=\"themeName\">The name of the theme.</param>\n        /// <returns>The path to the theme's directory.</returns>\n        public string GetThemePath(string themeName)\n        {\n            var themeSection = clientDefinitionsIni.GetSection(\"Themes\");\n            foreach (var key in themeSection.Keys)\n            {\n                var (name, path) = key.Value.Split(',');\n                if (name == themeName)\n                    return path;\n            }\n\n            return null;\n        }\n\n        public string SettingsIniName => clientDefinitionsIni.GetStringValue(SETTINGS, \"SettingsFile\", \"Settings.ini\");\n\n        public string TranslationIniName => clientDefinitionsIni.GetStringValue(TRANSLATIONS, nameof(TranslationIniName), \"Translation.ini\");\n\n        public string TranslationsFolderPath => SafePath.CombineDirectoryPath(\n            clientDefinitionsIni.GetStringValue(TRANSLATIONS, \"TranslationsFolder\",\n                SafePath.CombineDirectoryPath(\"Resources\", \"Translations\")));\n\n        private List<TranslationGameFile> _translationGameFiles;\n\n        public List<TranslationGameFile> TranslationGameFiles => _translationGameFiles;\n\n        /// <summary>\n        /// Force a refresh of the translation game files list.\n        /// </summary>\n        public void RefreshTranslationGameFiles()\n        {\n            _translationGameFiles = ParseTranslationGameFiles();\n        }\n\n        /// <summary>\n        /// Looks up the list of files to try and copy into the game folder with a translation.\n        /// </summary>\n        /// <returns>Source/destination relative path pairs.</returns>\n        /// <exception cref=\"IniParseException\">Thrown when the syntax of the list is invalid.</exception>\n        private List<TranslationGameFile> ParseTranslationGameFiles()\n        {\n            List<TranslationGameFile> gameFiles = new();\n            if (!clientDefinitionsIni.SectionExists(TRANSLATIONS))\n                return gameFiles;\n\n            foreach (string key in clientDefinitionsIni.GetSectionKeys(TRANSLATIONS))\n            {\n                // the syntax is GameFileX=path/to/source.file,path/to/destination.file[,checked]\n                // where X can be any text or number\n                if (!key.StartsWith(\"GameFile\"))\n                    continue;\n\n                string value = clientDefinitionsIni.GetStringValue(TRANSLATIONS, key, string.Empty);\n                string[] parts = clientDefinitionsIni.GetStringListValue(TRANSLATIONS, key, string.Empty);\n\n                // fail explicitly if the syntax is wrong\n                if (parts.Length is < 2 or > 3\n                    || (parts.Length == 3 && parts[2].ToUpperInvariant() != \"CHECKED\"))\n                {\n                    throw new IniParseException($\"Invalid syntax for value of {key}! \" +\n                        $\"Expected path/to/source.file,path/to/destination.file[,checked], read {value}.\");\n                }\n\n                bool isChecked = parts.Length == 3 && parts[2].ToUpperInvariant() == \"CHECKED\";\n\n                gameFiles.Add(new(Source: parts[0].Trim(), Target: parts[1].Trim(), isChecked));\n            }\n\n            return gameFiles;\n        }\n\n        public string ExtraExeCommandLineParameters => clientDefinitionsIni.GetStringValue(SETTINGS, \"ExtraCommandLineParams\", string.Empty);\n\n        public string MPMapsIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, \"MPMapsPath\", SafePath.CombineFilePath(\"INI\", \"MPMaps.ini\")));\n\n        public string KeyboardINI => clientDefinitionsIni.GetStringValue(SETTINGS, \"KeyboardINI\", \"Keyboard.ini\");\n\n        public bool SettingsIniAsKeyboardIni => SettingsIniName == KeyboardINI;\n\n        public string KeyboardHotkeySection => clientDefinitionsIni.GetStringValue(\n            SETTINGS,\n            \"KeyboardHotkeySection\",\n            ClientGameType == ClientType.RA ? \"WinHotKeys\" : \"Hotkey\");\n\n        public int MinimumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, \"MinimumIngameWidth\", 640);\n\n        public int MinimumIngameHeight => clientDefinitionsIni.GetIntValue(SETTINGS, \"MinimumIngameHeight\", 480);\n\n        public int MaximumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, \"MaximumIngameWidth\", int.MaxValue);\n\n        public int MaximumIngameHeight => clientDefinitionsIni.GetIntValue(SETTINGS, \"MaximumIngameHeight\", int.MaxValue);\n\n        public bool CopyMissionsToSpawnmapINI => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"CopyMissionsToSpawnmapINI\", true);\n\n        public string AllowedCustomGameModes => clientDefinitionsIni.GetStringValue(SETTINGS, \"AllowedCustomGameModes\", \"Standard,Custom Map\");\n\n        public int InactiveHostWarningMessageSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, \"InactiveHostWarningMessageSeconds\", 0);\n\n        public int InactiveHostKickSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, \"InactiveHostKickSeconds\", 0) + InactiveHostWarningMessageSeconds;\n\n        public bool InactiveHostKickEnabled => InactiveHostWarningMessageSeconds > 0 && InactiveHostKickSeconds > 0;\n\n        public string SkillLevelOptions => clientDefinitionsIni.GetStringValue(SETTINGS, \"SkillLevelOptions\", \"Any,Beginner,Intermediate,Pro\");\n\n        public int DefaultSkillLevelIndex => clientDefinitionsIni.GetIntValue(SETTINGS, \"DefaultSkillLevelIndex\", 0);\n\n        public bool CampaignTagSelectorEnabled => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"CampaignTagSelectorEnabled\", false);\n\n        public bool ReturnToMainMenuOnMissionLaunch => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"ReturnToMainMenuOnMissionLaunch\", true);\n\n        public string GetGameExecutableName()\n        {\n            string[] exeNames = clientDefinitionsIni.GetStringListValue(SETTINGS, \"GameExecutableNames\", \"Game.exe\");\n\n            return exeNames[0];\n        }\n\n        public string GameLauncherExecutableName => clientDefinitionsIni.GetStringValue(SETTINGS, \"GameLauncherExecutableName\", string.Empty);\n\n        public string[] GetCompatibilityCheckExecutables()\n        {\n            string[] exeNames = clientDefinitionsIni.GetStringListValue(SETTINGS, \"CompatibilityCheckExecutables\", string.Empty);\n\n            return exeNames;\n        }\n\n        public bool SaveSkirmishGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"SaveSkirmishGameOptions\", false);\n\n        public bool SaveCampaignGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"SaveCampaignGameOptions\", false);\n\n        public bool CreateSavedGamesDirectory => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"CreateSavedGamesDirectory\", false);\n\n        public bool DisableMultiplayerGameLoading => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"DisableMultiplayerGameLoading\", false);\n\n        public bool DisplayPlayerCountInTopBar => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"DisplayPlayerCountInTopBar\", false);\n\n        /// <summary>\n        /// The name of the executable in the main game directory that selects\n        /// the correct main client executable.\n        /// For example, DTA.exe in case of DTA.\n        /// </summary>\n        public string LauncherExe => clientDefinitionsIni.GetStringValue(SETTINGS, \"LauncherExe\", string.Empty);\n\n        public bool UseClientRandomStartLocations => clientDefinitionsIni.GetBooleanValue(SETTINGS, \"UseClientRandomStartLocations\", false);\n\n        /// <summary>\n        /// Returns the name of the game executable file that is used on\n        /// Linux and macOS.\n        /// </summary>\n        public string UnixGameExecutableName => clientDefinitionsIni.GetStringValue(SETTINGS, \"UnixGameExecutableName\", \"wine-dta.sh\");\n\n        /// <summary>\n        /// List of files that are not distributed but required to play.\n        /// </summary>\n        public string[] RequiredFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, \"RequiredFiles\", String.Empty);\n\n        /// <summary>\n        /// List of files that can interfere with the mod functioning.\n        /// </summary>\n        public string[] ForbiddenFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, \"ForbiddenFiles\", String.Empty);\n\n        /// <summary>\n        /// The main map file extension that is read by the client.\n        /// </summary>\n        public string MapFileExtension => clientDefinitionsIni.GetStringValue(SETTINGS, \"MapFileExtension\", \"map\");\n\n        /// <summary>\n        /// This tells the client which supplemental map files are ok to copy over during \"spawnmap.ini\" file creation.\n        /// IE, if \"BIN\" is listed, then the client will look for and copy the file \"map_a.bin\"\n        /// when writing the spawnmap.ini file for map file \"map_a.ini\".\n        /// \n        /// This configuration should be in the form \"SupplementalMapFileExtensions=bin,mix\"\n        /// </summary>\n        public IEnumerable<string> SupplementalMapFileExtensions\n            => clientDefinitionsIni.GetStringValue(SETTINGS, \"SupplementalMapFileExtensions\", null)?\n                .Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();\n\n        /// <summary>\n        /// This prevents users from joining games that are incompatible/on a different game version than the current user.\n        /// Default: false\n        /// </summary>\n        public bool DisallowJoiningIncompatibleGames => clientDefinitionsIni.GetBooleanValue(SETTINGS, nameof(DisallowJoiningIncompatibleGames), false);\n\n        /// <summary>\n        /// Activates warnings for development builds of XNA Client\n        /// </summary>\n        public bool ShowDevelopmentBuildWarnings => clientDefinitionsIni.GetBooleanValue(SETTINGS, nameof(ShowDevelopmentBuildWarnings), true);\n\n        #endregion\n\n        #region Network definitions\n\n        public string CnCNetTunnelListURL => networkDefinitionsIni.GetStringValue(SETTINGS, \"CnCNetTunnelListURL\", \"https://cncnet.org/master-list\");\n\n        public string CnCNetPlayerCountURL => networkDefinitionsIni.GetStringValue(SETTINGS, \"CnCNetPlayerCountURL\", \"https://api.cncnet.org/status\");\n\n        public string CnCNetMapDBDownloadURL => networkDefinitionsIni.GetStringValue(SETTINGS, \"CnCNetMapDBDownloadURL\", \"https://mapdb.cncnet.org\");\n\n        public string CnCNetMapDBUploadURL => networkDefinitionsIni.GetStringValue(SETTINGS, \"CnCNetMapDBUploadURL\", \"https://mapdb.cncnet.org/upload\");\n\n        public bool DisableDiscordIntegration => networkDefinitionsIni.GetBooleanValue(SETTINGS, \"DisableDiscordIntegration\", false);\n\n        public List<string> IRCServers => GetIRCServers();\n\n        #endregion\n\n        #region User default settings\n\n        public bool UserDefault_BorderlessWindowedClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, \"BorderlessWindowedClient\", true);\n\n        public bool UserDefault_IntegerScaledClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, \"IntegerScaledClient\", false);\n\n        public bool UserDefault_WriteInstallationPathToRegistry => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, \"WriteInstallationPathToRegistry\", true);\n\n        #endregion\n\n        #region Game networking defaults\n\n        /// <summary>\n        /// Default value for FrameSendRate setting written in spawn.ini.\n        /// </summary>\n        public int DefaultFrameSendRate => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultFrameSendRate), 7);\n\n        /// <summary>\n        /// Default value for Protocol setting written in spawn.ini.\n        /// </summary>\n        public int DefaultProtocolVersion => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultProtocolVersion), 2);\n\n        /// <summary>\n        /// Default value for MaxAhead setting written in spawn.ini.\n        /// </summary>\n        public int DefaultMaxAhead => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultMaxAhead), 0);\n\n        #endregion\n\n        public List<string> GetIRCServers()\n        {\n            List<string> servers = [];\n\n            IniSection serversSection = networkDefinitionsIni.GetSection(\"IRCServers\");\n            if (serversSection != null)\n                foreach ((_, string value) in serversSection.Keys)\n                    if (!string.IsNullOrWhiteSpace(value))\n                        servers.Add(value);\n\n            return servers;\n        }\n\n        public bool DiscordIntegrationGloballyDisabled => string.IsNullOrWhiteSpace(DiscordAppId) || DisableDiscordIntegration;\n\n        public string CustomMissionPath => clientDefinitionsIni.GetStringValue(SETTINGS, \"CustomMissionPath\", \"Maps/CustomMissions\");\n\n        public List<(string extension, string copyAs)> GetCustomMissionSupplementFiles()\n        {\n            List<(string extension, string copyAs)> files = new();\n            Dictionary<string, int> extensionToIndex = new(StringComparer.OrdinalIgnoreCase);\n\n            int index = 0;\n            while (true)\n            {\n                string extensionKey = $\"CustomMissionSupplementFile{index}Extension\";\n                string copyAsKey = $\"CustomMissionSupplementFile{index}CopyAs\";\n\n                string extension = clientDefinitionsIni.GetStringValue(SETTINGS, extensionKey, null)?.Trim();\n\n                // Stop iteration if the extension key is missing\n                if (string.IsNullOrWhiteSpace(extension))\n                    break;\n\n                string copyAs = clientDefinitionsIni.GetStringValue(SETTINGS, copyAsKey, null);\n\n                // Validate that copyAs is not empty\n                if (string.IsNullOrWhiteSpace(copyAs))\n                    throw new ClientConfigurationException($\"Configuration key '{copyAsKey}' is required when '{extensionKey}' is present for supplement file {index}.\");\n\n                // Validate that extension is unique\n                if (extensionToIndex.TryGetValue(extension, out int firstIndex))\n                    throw new ClientConfigurationException($\"Duplicate extension '{extension}' found in supplement files. Extension is used in both file {firstIndex} and file {index}.\");\n\n                extensionToIndex.Add(extension, index);\n                files.Add((extension, copyAs));\n                index++;\n            }\n\n            return files;\n        }\n\n        public OSVersion GetOperatingSystemVersion()\n        {\n#if NETFRAMEWORK\n            // OperatingSystem.IsWindowsVersionAtLeast() is the preferred API but is not supported on earlier .NET versions\n            if (Environment.OSVersion.Platform == PlatformID.Win32NT)\n            {\n                Version osVersion = Environment.OSVersion.Version;\n\n                if (osVersion.Major <= 4)\n                    return OSVersion.UNKNOWN;\n\n                if (osVersion.Major == 5)\n                    return OSVersion.WINXP;\n\n                if (osVersion.Major == 6 && osVersion.Minor == 0)\n                    return OSVersion.WINVISTA;\n\n                if (osVersion.Major == 6 && osVersion.Minor <= 1)\n                    return OSVersion.WIN7;\n\n                return OSVersion.WIN810;\n            }\n\n            if (ProgramConstants.ISMONO)\n                return OSVersion.UNIX;\n\n            // http://mono.wikia.com/wiki/Detecting_the_execution_platform\n            int p = (int)Environment.OSVersion.Platform;\n            if (p == 4 || p == 6 || p == 128)\n                return OSVersion.UNIX;\n\n            return OSVersion.UNKNOWN;\n#else\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                if (OperatingSystem.IsWindowsVersionAtLeast(6, 2))\n                    return OSVersion.WIN810;\n                else if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))\n                    return OSVersion.WIN7;\n                else if (OperatingSystem.IsWindowsVersionAtLeast(6, 0))\n                    return OSVersion.WINVISTA;\n                else if (OperatingSystem.IsWindowsVersionAtLeast(5, 0))\n                    return OSVersion.WINXP;\n                else\n                    return OSVersion.UNKNOWN;\n            }\n\n            return OSVersion.UNIX;\n#endif\n        }\n    }\n\n    /// <summary>\n    /// An exception that is thrown when a client configuration file contains invalid or\n    /// unexpected settings / data or required settings / data are missing.\n    /// </summary>\n    public class ClientConfigurationException : Exception\n    {\n        public ClientConfigurationException(string message) : base(message)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/ClientCore.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <Description>CnCNet Client Core Library</Description>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"System.Text.Encoding.CodePages\" />\n    <PackageReference Include=\"Ude.NetStandard\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Rampastring.XNAUI\\Rampastring.Tools\\Rampastring.Tools.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "ClientCore/Enums/AllowPrivateMessagesFromEnum.cs",
    "content": "﻿namespace ClientCore.Enums\n{\n    public enum AllowPrivateMessagesFromEnum\n    {\n        All = 1,\n        CurrentChannel = 4,\n        Friends = 2,\n        None = 3,\n    }\n}\n"
  },
  {
    "path": "ClientCore/Enums/ClientType.cs",
    "content": "﻿namespace ClientCore.Enums\n{\n    public enum ClientType\n    {\n        TS,\n        YR,\n        Ares,\n        RA,\n    }\n}\n"
  },
  {
    "path": "ClientCore/Enums/ClientTypeHelper.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing ClientCore.Extensions;\n\nnamespace ClientCore.Enums\n{\n    public static class ClientTypeHelper\n    {\n        public static ClientType FromString(string value) => value switch\n        {\n            \"TS\" => ClientType.TS,\n            \"YR\" => ClientType.YR,\n            \"Ares\" => ClientType.Ares,\n            \"RA\" => ClientType.RA,\n            _ => throw new Exception(string.Format((\n                \"It seems the client configuration was not migrated to accommodate for the v2.12 changes. \" +\n                \"Please specify 'ClientGameType' in '[Settings]' section of the 'ClientDefinitions.ini' file \" +\n                \"(allowed options: {0}).\\n\\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\"),\n                EnumExtensions.GetNames<ClientType>(),\n                \"https://github.com/CnCNet/xna-cncnet-client/\")),\n        };\n    }\n}\n"
  },
  {
    "path": "ClientCore/Enums/SortDirection.cs",
    "content": "﻿namespace ClientCore.Enums\n{\n    public enum SortDirection\n    {\n        None = 0,\n        Asc = 1,\n        Desc = 2\n    }\n}\n"
  },
  {
    "path": "ClientCore/Extensions/ArrayExtensions.cs",
    "content": "﻿using System;\n\nnamespace ClientCore.Extensions;\n\n// https://stackoverflow.com/a/65894979/20766970\npublic static class ArrayExtensions\n{\n    public static void Deconstruct<T>(this T[] @this, out T a0)\n    {\n        if (@this == null || @this.Length < 1)\n            throw new ArgumentException(null, nameof(@this));\n\n        a0 = @this[0];\n    }\n\n    public static (T, T) AsTuple2<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 2)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1)\n        => (a0, a1) = @this.AsTuple2();\n\n    public static (T, T, T) AsTuple3<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 3)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1, out T a2)\n        => (a0, a1, a2) = @this.AsTuple3();\n\n    public static (T, T, T, T) AsTuple4<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 4)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2], @this[3]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1, out T a2, out T a3)\n        => (a0, a1, a2, a3) = @this.AsTuple4();\n\n    public static (T, T, T, T, T) AsTuple5<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 5)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2], @this[3], @this[4]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4)\n        => (a0, a1, a2, a3, a4) = @this.AsTuple5();\n\n    public static (T, T, T, T, T, T) AsTuple6<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 6)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4, out T a5)\n        => (a0, a1, a2, a3, a4, a5) = @this.AsTuple6();\n\n    public static (T, T, T, T, T, T, T) AsTuple7<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 7)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5], @this[6]);\n    }\n\n    public static void Deconstruct<T>(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4, out T a5, out T a6)\n        => (a0, a1, a2, a3, a4, a5, a6) = @this.AsTuple7();\n\n    public static (T, T, T, T, T, T, T, T) AsTuple8<T>(this T[] @this)\n    {\n        if (@this == null || @this.Length < 8)\n            throw new ArgumentException(null, nameof(@this));\n\n        return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5], @this[6], @this[7]);\n    }\n\n    public static void Deconstruct<T>(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)\n        => (a0, a1, a2, a3, a4, a5, a6, a7) = @this.AsTuple8();\n}\n"
  },
  {
    "path": "ClientCore/Extensions/EnumExtensions.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Text;\n\nnamespace ClientCore.Extensions\n{\n    public static class EnumExtensions\n    {\n        public static T CycleNext<T>(this T src) where T : Enum\n        {\n            T[] values = EnumExtensions.GetValues<T>();\n            return values[(Array.IndexOf(values, src) + 1) % values.Length];\n        }\n\n        public static T First<T>() where T : Enum \n            => EnumExtensions.GetValues<T>()[0];\n\n        public static string GetNames<T>() where T : Enum\n            => string.Join(\", \", EnumExtensions.GetValues<T>().Select(e => e.ToString()));\n\n        private static T[] GetValues<T>() where T : Enum \n            => (T[])Enum.GetValues(typeof(T));\n    }\n}\n"
  },
  {
    "path": "ClientCore/Extensions/EnumerableExtensions.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\n\nnamespace ClientCore.Extensions;\n\npublic static class EnumerableExtensions\n{\n    /// <summary>\n    /// Converts an enumerable to a matrix of items with a max number of items per column.\n    /// The matrix is built column by column, left to right.\n    /// </summary>\n    /// <param name=\"enumerable\">the enumerable to convert</param>\n    /// <param name=\"maxPerColumn\">the max number of items per column</param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public static List<List<T>> ToMatrix<T>(this IEnumerable<T> enumerable, int maxPerColumn)\n    {\n        var list = enumerable.ToList();\n        return list.Aggregate(new List<List<T>>(), (matrix, item) =>\n        {\n            int index = list.IndexOf(item);\n            int column = (index / maxPerColumn);\n            List<T> columnList = matrix.Count <= column ? new List<T>() : matrix[column];\n            if (columnList.Count == 0)\n                matrix.Add(columnList);\n\n            columnList.Add(item);\n            return matrix;\n        });\n    }\n}"
  },
  {
    "path": "ClientCore/Extensions/FileExtensions.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Versioning;\nusing System.Text;\nusing Rampastring.Tools;\nusing ClientCore.PlatformShim;\n\nnamespace ClientCore.Extensions;\n\npublic class FileExtensions\n{\n\n    /// <summary>\n    /// 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.\n    /// <br/>\n    /// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw\n    /// </summary>\n    /// <param name=\"lpFileName\">The name of the new file.</param>\n    /// <param name=\"lpExistingFileName\">The name of the existing file.</param>\n    /// <param name=\"lpSecurityAttributes\">Reserved; must be NULL.</param>\n    /// <returns>If the function succeeds, the return value is nonzero. If the function fails, the return value is zero (0).</returns>\n    [DllImport(\"Kernel32.dll\", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = \"CreateHardLinkW\")]\n    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]\n    [SupportedOSPlatform(\"windows\")]\n    private static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);\n\n    /// <summary>\n    /// The link function makes a new link to the existing file named by oldname, under the new name newname.\n    /// <br/>\n    /// https://www.gnu.org/software/libc/manual/html_node/Hard-Links.html\n    /// <param name=\"oldname\"></param>\n    /// <param name=\"newname\"></param>\n    /// <returns>This function returns a value of 0 if it is successful and -1 on failure.</returns>\n    [DllImport(\"libc\", EntryPoint = \"link\", SetLastError = true)]\n    [SupportedOSPlatform(\"linux\")]\n    [SupportedOSPlatform(\"osx\")]\n    private static extern int link([MarshalAs(UnmanagedType.LPUTF8Str)] string oldname, [MarshalAs(UnmanagedType.LPUTF8Str)] string newname);\n\n    /// <summary>\n    /// Creates hard link to the source file or copy that file, if got an error.\n    /// </summary>\n    /// <param name=\"source\"></param>\n    /// <param name=\"destination\"></param>\n    /// <param name=\"fallback\"></param>\n    /// <exception cref=\"IOException\"></exception>\n    /// <exception cref=\"PlatformNotSupportedException\"></exception>\n    public static void CreateHardLinkFromSource(string source, string destination, bool fallback = true)\n    {\n        if (fallback)\n        {\n            try\n            {\n                CreateHardLinkFromSource(source, destination, fallback: false);\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"Failed to create hard link at {destination}. Fallback to copy. {ex.Message}\");\n                File.Copy(source, destination, true);\n            }\n\n            return;\n        }\n\n        if (File.Exists(destination))\n        {\n            FileInfo destinationFile = new(destination);\n            destinationFile.IsReadOnly = false;\n            destinationFile.Delete();\n        }\n\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n        {\n            if (!CreateHardLink(destination, source, IntPtr.Zero))\n                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());\n        }\n        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            if (link(source, destination) != 0)\n                throw new IOException(string.Format(\"Unable to create hard link at {0} with the following error code: {1}\"\n                    .L10N(\"Client:DTAConfig:CreateHardLinkFailed\"), destination, Marshal.GetLastWin32Error()));\n        }\n        else\n        {\n            throw new PlatformNotSupportedException();\n        }\n    }\n\n    /// <summary>\n    /// Predicts text file encoding by its content.\n    /// </summary>\n    /// <param name=\"filename\"></param>\n    /// <param name=\"minimalConfidence\"></param>\n    /// <returns></returns>\n    public static Encoding GetDetectedEncoding(string filename, float minimalConfidence = 0.5f)\n    {\n        Encoding encoding = EncodingExt.UTF8NoBOM;\n\n        using (FileStream fs = File.OpenRead(filename))\n        {\n            Ude.CharsetDetector cdet = new Ude.CharsetDetector();\n            cdet.Feed(fs);\n            cdet.DataEnd();\n            if (cdet.Charset != null && cdet.Confidence > minimalConfidence)\n            {\n                Encoding detectedEncoding = Encoding.GetEncoding(cdet.Charset);\n\n                if (detectedEncoding is not UTF8Encoding and not ASCIIEncoding)\n                    encoding = detectedEncoding;\n            }\n        }\n\n        return encoding;\n    }\n}\n"
  },
  {
    "path": "ClientCore/Extensions/IniFileExtensions.cs",
    "content": "﻿#nullable enable\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing Rampastring.Tools;\n\nnamespace ClientCore.Extensions\n{\n    public static class IniFileExtensions\n    {\n        extension(IniFile iniFile)\n        {\n            // Clone() method is not officially available now. https://github.com/Rampastring/Rampastring.Tools/issues/12\n            public IniFile Clone()\n            {\n                var newIni = new IniFile();\n                foreach (string sectionName in iniFile.GetSections())\n                {\n                    IniSection oldSection = iniFile.GetSection(sectionName);\n                    newIni.AddSection(oldSection.Clone());\n                }\n\n                return newIni;\n            }\n\n            public IniSection GetOrAddSection(string sectionName)\n            {\n                var section = iniFile.GetSection(sectionName);\n                if (section != null)\n                    return section;\n\n                section = new IniSection(sectionName);\n                iniFile.AddSection(section);\n                return section;\n            }\n\n            public string[] GetStringListValue(string section, string key, string defaultValue, char[]? separators = null)\n                => (iniFile.GetSection(section)?.GetStringValue(key, defaultValue) ?? defaultValue)\n                    .SplitWithCleanup(separators);\n\n        }\n\n        extension(IniSection iniSection)\n        {\n            public IniSection Clone()\n            {\n                IniSection newSection = new(iniSection.SectionName);\n\n                foreach ((var key, var value) in iniSection.Keys)\n                {\n                    newSection.AddKey(key, value);\n                }\n\n                return newSection;\n            }\n\n            public void RemoveAllKeys()\n            {\n                var keys = new List<KeyValuePair<string, string>>(iniSection.Keys);\n                foreach (KeyValuePair<string, string> iniSectionKey in keys)\n                    iniSection.RemoveKey(iniSectionKey.Key);\n            }\n            \n            public string? GetStringValueOrNull(string key) =>\n                iniSection.KeyExists(key) ? iniSection.GetStringValue(key, string.Empty) : null;\n\n            public int? GetIntValueOrNull(string key) =>\n                iniSection.KeyExists(key) ? iniSection.GetIntValue(key, 0) : null;\n\n            public bool? GetBooleanValueOrNull(string key) =>\n                iniSection.KeyExists(key) ? iniSection.GetBooleanValue(key, false) : null;\n\n            public List<T>? GetListValueOrNull<T>(string key, char separator, Func<string, T> converter) =>\n                iniSection.KeyExists(key) ? iniSection.GetListValue<T>(key, separator, converter) : null;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Extensions/StringExtensions.cs",
    "content": "﻿using System;\nusing System.Text.RegularExpressions;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore.I18N;\n\nnamespace ClientCore.Extensions;\n\npublic static class StringExtensions\n{\n    private static Regex extractLinksRE = new Regex(@\"((http[s]?)|(ftp))\\S+\");\n\n    public static string[] GetLinks(this string text)\n    {\n        if (string.IsNullOrWhiteSpace(text))\n            return null;\n\n        var matches = extractLinksRE.Matches(text);\n\n        if (matches.Count == 0)\n            return null; // No link found\n\n        string[] links = new string[matches.Count];\n        for (int i = 0; i < links.Length; i++)\n            links[i] = matches[i].Value.Trim();\n            \n        return links;\n    }\n\n    private const string ESCAPED_INI_NEWLINE_PATTERN = $\"\\\\{ProgramConstants.INI_NEWLINE_PATTERN}\";\n    private const string ESCAPED_SEMICOLON = \"\\\\semicolon\";\n\n    /// <summary>\n    /// Converts a regular string to an INI representation of it.\n    /// </summary>\n    /// <param name=\"raw\">Input string.</param>\n    /// <returns>INI-safe string.</returns>\n    public static string ToIniString(this string raw)\n    {\n        if (raw.Contains(ESCAPED_INI_NEWLINE_PATTERN, StringComparison.InvariantCulture))\n            throw new ArgumentException($\"The string contains an illegal character sequence! ({ESCAPED_INI_NEWLINE_PATTERN})\");\n\n        if (raw.Contains(ESCAPED_SEMICOLON, StringComparison.InvariantCulture))\n            throw new ArgumentException($\"The string contains an illegal character sequence! ({ESCAPED_SEMICOLON})\");\n\n        return raw\n            .Replace(ProgramConstants.INI_NEWLINE_PATTERN, ESCAPED_INI_NEWLINE_PATTERN)\n            .Replace(\";\", ESCAPED_SEMICOLON)\n            .Replace(Environment.NewLine, \"\\n\")\n            .Replace(\"\\n\", ProgramConstants.INI_NEWLINE_PATTERN);\n    }\n\n    /// <summary>\n    /// Converts an INI-safe string to a normal string.\n    /// </summary>\n    /// <param name=\"iniString\">Input INI string.</param>\n    /// <returns>Regular string.</returns>\n    public static string FromIniString(this string iniString)\n    {\n        return iniString\n            .Replace(ESCAPED_INI_NEWLINE_PATTERN, ProgramConstants.INI_NEWLINE_PATTERN)\n            .Replace(ESCAPED_SEMICOLON, \";\")\n            .Replace(ProgramConstants.INI_NEWLINE_PATTERN, Environment.NewLine);\n    }\n\n    /// <summary>\n    /// Looks up a translated string for the specified key.\n    /// </summary>\n    /// <param name=\"defaultValue\">The default string value as a fallback.</param>\n    /// <param name=\"key\">The unique key name.</param>\n    /// <param name=\"notify\">Whether to add this key and value to the list of missing key-values.</param>\n    /// <returns>The translated string value.</returns>\n    /// <remarks>\n    /// This method is referenced by <c>TranslationNotifierGenerator</c> in order to check if the const\n    /// values that are not initialized on client start automatically are missing (via notification\n    /// mechanism implemented down the call chain). Do not change the signature or move the method out\n    /// of the namespace it's currently defined in. If you do - you have to also edit the generator\n    /// source code to match.\n    /// </remarks>\n    public static string L10N(this string defaultValue, string key, bool notify = true)\n        => string.IsNullOrEmpty(defaultValue)\n            ? defaultValue\n            : Translation.Instance.LookUp(key, defaultValue, notify);\n\n    /// <summary>\n    /// Replace special characters with spaces in the filename to avoid conflicts with WIN32API.\n    /// </summary>\n    /// <param name=\"defaultValue\">The default string value.</param>\n    /// <returns>File name without special characters or reserved combinations.</returns>\n    /// <remarks>\n    /// Reference: <a href=\"https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file\">Naming Files, Paths, and Namespaces</a>.\n    /// </remarks>\n    public static string ToWin32FileName(this string filename)\n    {\n        foreach (char ch in \"/\\\\:*?<>|\")\n            filename = filename.Replace(ch, '_');\n\n        // If the user is somehow using \"con\" or any other filename that is\n        // reserved by WIN32API, it would be better to rename it.\n\n        HashSet<string> reservedFileNames = new HashSet<string>(new List<string>(){\n            \"CON\",\n            \"PRN\",\n            \"AUX\",\n            \"NUL\",\n            \"COM1\", \"COM2\", \"COM3\", \"COM4\", \"COM5\", \"COM6\", \"COM7\", \"COM8\", \"COM9\", \"COM¹\", \"COM²\", \"COM³\",\n            \"LPT1\", \"LPT2\", \"LPT3\", \"LPT4\", \"LPT5\", \"LPT6\", \"LPT7\", \"LPT8\", \"LPT9\", \"LPT¹\", \"LPT²\", \"LPT³\"\n        }, StringComparer.InvariantCultureIgnoreCase);\n\n        if (reservedFileNames.Contains(filename))\n            filename += \"_\";\n\n        return filename;\n    }\n  \n    public static T ToEnum<T>(this string value) where T : Enum \n        => (T)Enum.Parse(typeof(T), value, true);\n\n    public static string[] SplitWithCleanup(this string value, char[] separators = null)\n        => value\n            .Split(separators ?? [','])\n            .Select(s => s.Trim())\n            .Where(s => !string.IsNullOrEmpty(s))\n            .ToArray();\n}\n"
  },
  {
    "path": "ClientCore/I18N/Translation.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nusing ClientCore.Extensions;\nusing ClientCore.PlatformShim;\n\nusing Rampastring.Tools;\n\nnamespace ClientCore.I18N;\n\npublic class Translation : ICloneable\n{\n    public static Translation Instance { get; set; } = new Translation(ProgramConstants.HARDCODED_LOCALE_CODE);\n\n    /// <summary>The translation metadata section name.</summary>\n    public const string METADATA_SECTION = \"General\";\n\n    private static CultureInfo _initialUICulture;\n    /// <summary>The UI culture that the application was started with. Must be initialized as early as possible.</summary>\n    public static CultureInfo InitialUICulture\n    {\n        get => _initialUICulture;\n        set => _initialUICulture = _initialUICulture is null ? value\n            : throw new InvalidOperationException($\"{nameof(InitialUICulture)} is already set!\");\n    }\n\n    /// <summary>AKA name of the folder, used to look up <see cref=\"Culture\"/> and select a language</summary>\n    public string LocaleCode { get; private set; } = string.Empty;\n\n    /// <summary>The explicitly set UI name for the translation.</summary>\n    private string _name = string.Empty;\n    /// <summary>The UI name for the translation.</summary>\n    public string Name\n    {\n        get => string.IsNullOrWhiteSpace(_name) ? GetLanguageName(LocaleCode) : _name;\n        private set => _name = value;\n    }\n\n    /// <summary>The explicitly set UI culture for the translation.</summary>\n    /// <remarks>Not accounted when selecting the translation automatically.</remarks>\n    private CultureInfo _culture;\n    /// <summary>The UI culture for the translation.</summary>\n    public CultureInfo Culture\n    {\n        get => _culture is null ? new CultureInfo(LocaleCode) : _culture;\n        private set => _culture = value;\n    }\n\n    /// <summary>The author(s) of the translation.</summary>\n    public string Author { get; private set; } = string.Empty;\n\n    /// <summary>Override the default encoding used for reading/writing map files. Null (\"Auto\") means detecting the encoding from each file (sometimes unreliable). </summary>\n    public Encoding MapEncoding = EncodingExt.UTF8NoBOM;\n\n    /// <summary>Stores the translation values (including default values for missing strings).</summary>\n    private Dictionary<string, string> Values { get; } = new();\n\n    // public bool IsRightToLeft { get; set; } // TODO\n\n    /// <summary>Contains all keys within <see cref=\"Values\"/> with missing translations.</summary>\n    private readonly HashSet<string> MissingKeys = new();\n\n    /// <summary>Used to write missing translation table entries to a file.</summary>\n    public const string MISSING_KEY_PREFIX = \"; \";  // a hack but hey it works\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Translation\"/> class.\n    /// </summary>\n    /// <param name=\"localeCode\">A locale code for this translation.</param>\n    public Translation(string localeCode)\n    {\n        LocaleCode = localeCode;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Translation\"/> class\n    /// that loads the translation from an INI file.\n    /// </summary>\n    /// <param name=\"ini\">An INI file to read from.</param>\n    /// <param name=\"localeCode\">A locale code for this translation.</param>\n    public Translation(IniFile ini, string localeCode)\n        : this(localeCode)\n    {\n        if (ini is null)\n            throw new ArgumentNullException(nameof(ini));\n\n        IniSection metadataSection = ini.GetSection(METADATA_SECTION);\n        Name = metadataSection?.GetStringValue(nameof(Name), string.Empty);\n        Author = metadataSection?.GetStringValue(nameof(Author), string.Empty);\n\n        MapEncoding = EncodingExt.GetEncodingWithAuto(metadataSection?.GetStringValue(nameof(MapEncoding), null));\n\n        string cultureName = metadataSection?.GetStringValue(nameof(Culture), null);\n        if (cultureName is not null)\n            Culture = new(cultureName);\n\n        AppendValuesFromIniFile(ini);\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Translation\"/> class\n    /// that loads the translation from an INI file.\n    /// </summary>\n    /// <param name=\"iniPath\">A path to an INI file to read from.</param>\n    /// <param name=\"localeCode\">A locale code for this translation.</param>\n    public Translation(string iniPath, string localeCode)\n        : this(new CCIniFile(iniPath), localeCode) { }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Translation\"/> class\n    /// that is a copy of the given instance.\n    /// </summary>\n    /// <param name=\"other\">An object to copy from.</param>\n    public Translation(Translation other)\n    {\n        LocaleCode = other.LocaleCode;\n        _name = other._name;\n        _culture = other._culture;\n        Author = other.Author;\n        MapEncoding = other.MapEncoding;\n\n        foreach (var (key, value) in other.Values)\n            Values.Add(key, value);\n    }\n\n    public Translation Clone() => new Translation(this);\n    object ICloneable.Clone() => Clone();\n\n    /// <summary>\n    /// Reads <see cref=\"Values\"/> from an INI file, overriding possibly existing ones.\n    /// </summary>\n    /// <param name=\"iniPath\">A path to an INI file to read from.</param>\n    public void AppendValuesFromIniFile(string iniPath)\n        => AppendValuesFromIniFile(new CCIniFile(iniPath));\n\n    /// <summary>\n    /// Reads <see cref=\"Values\"/> from an INI file, overriding possibly existing ones.\n    /// </summary>\n    /// <param name=\"ini\">An INI file to read from.</param>\n    public void AppendValuesFromIniFile(IniFile ini)\n    {\n        IniSection valuesSection = ini.GetSection(nameof(Values));\n        foreach (var (key, value) in valuesSection.Keys)\n            Values[key] = value.FromIniString();\n    }\n\n    /// <param name=\"localeCode\">The locale code to look up the language name for.</param>\n    /// <returns>The language name for the given locale code.</returns>\n    public static string GetLanguageName(string localeCode)\n    {\n        string result = null;\n\n        string iniPath = SafePath.CombineFilePath(\n            ClientConfiguration.Instance.TranslationsFolderPath, localeCode, ClientConfiguration.Instance.TranslationIniName);\n\n        if (SafePath.GetFile(iniPath).Exists)\n        {\n            // This parses only the metadata section content so that we don't parse\n            // the bazillion of localized values just to read the translation name.\n            // The only issue is that inheritance would break.\n            // FIXME AllowNewSections is ignored with inheritance\n            IniFile ini = new();\n            ini.AddSection(METADATA_SECTION);\n            ini.FileName = iniPath;\n            ini.AllowNewSections = false;\n\n            ini.Parse();\n\n            // Overridden name first\n            IniSection metadataSection = ini.GetSection(METADATA_SECTION);\n            result = metadataSection?.GetStringValue(nameof(Name), null);\n        }\n\n        if (string.IsNullOrWhiteSpace(result))\n            result = new CultureInfo(localeCode).NativeName;\n\n        if (string.IsNullOrWhiteSpace(result))\n            result = localeCode;\n\n        return result;\n    }\n\n    /// <summary>\n    /// Applies (hard-links or copies) the translation game files for a given locale to the game directory,\n    /// and removes any destination files whose source no longer exists.\n    /// </summary>\n    public void ApplyTranslationGameFiles() => ApplyTranslationGameFiles(LocaleCode);\n\n    /// <inheritdoc cref=\"ApplyTranslationGameFiles()\"/>\n    /// <param name=\"localeCode\">The locale code identifying the translation whose game files should be applied.</param>\n    public static void ApplyTranslationGameFiles(string localeCode)\n    {\n        ClientConfiguration.Instance.RefreshTranslationGameFiles();\n\n        string translationFolderPath = SafePath.CombineDirectoryPath(\n            ClientConfiguration.Instance.TranslationsFolderPath, localeCode);\n\n        foreach (var tgf in ClientConfiguration.Instance.TranslationGameFiles)\n        {\n            string sourcePath = SafePath.CombineFilePath(translationFolderPath, tgf.Source);\n            string targetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, tgf.Target);\n\n            if (File.Exists(sourcePath))\n            {\n                string sourceHash = Utilities.CalculateSHA1ForFile(sourcePath);\n                string targetHash = Utilities.CalculateSHA1ForFile(targetPath);\n\n                if (sourceHash != targetHash)\n                {\n                    FileExtensions.CreateHardLinkFromSource(sourcePath, targetPath);\n                    new FileInfo(targetPath).IsReadOnly = true;\n                }\n            }\n            else\n            {\n                if (File.Exists(targetPath))\n                {\n                    new FileInfo(targetPath).IsReadOnly = false;\n                    File.Delete(targetPath);\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// Lists valid available translations from the <see cref=\"TranslationsFolderPath\"/> along with their UI names.\n    /// A localization is valid if it has a corresponding <see cref=\"TranslationIniName\"/> file in the <see cref=\"TranslationsFolderPath\"/>.\n    /// </summary>\n    /// <returns>Locale code -> display name pairs.</returns>\n    public static Dictionary<string, string> GetTranslations()\n    {\n        var translations = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)\n        {\n            // Add default localization so that we always have it in the list even if the localization does not exist\n            [ProgramConstants.HARDCODED_LOCALE_CODE] = GetLanguageName(ProgramConstants.HARDCODED_LOCALE_CODE)\n        };\n\n        if (!Directory.Exists(ClientConfiguration.Instance.TranslationsFolderPath))\n            return translations;\n\n        foreach (var localizationFolder in Directory.GetDirectories(ClientConfiguration.Instance.TranslationsFolderPath))\n        {\n            string localizationCode = Path.GetFileName(localizationFolder);\n            translations[localizationCode] = GetLanguageName(localizationCode);\n        }\n\n        return translations;\n    }\n\n    /// <summary>\n    /// Checks the current UI culture and finds the closest match from supported translations.\n    /// </summary>\n    /// <returns>Available translation locale code.</returns>\n    public static string GetDefaultTranslationLocaleCode()\n    {\n        // we don't need names here pretty much\n        Dictionary<string, string> translations = GetTranslations();\n\n        for (var culture = InitialUICulture;\n            culture != CultureInfo.InvariantCulture;\n            culture = culture.Parent)\n        {\n            string translation = culture.Name;\n\n            // the keys in 'translations' are case-insensitive\n            if (translations.ContainsKey(translation))\n                return translation;\n        }\n\n        return ProgramConstants.HARDCODED_LOCALE_CODE;\n    }\n\n    /// <summary>\n    /// Dump the translation table to an ini file.\n    /// </summary>\n    /// <returns>An ini file that contains the translation table.</returns>\n    public IniFile DumpIni(bool saveOnlyMissingValues = false)\n    {\n        IniFile ini = new IniFile();\n\n        ini.AddSection(METADATA_SECTION);\n        IniSection general = ini.GetSection(METADATA_SECTION);\n\n        if (!string.IsNullOrWhiteSpace(_name))\n            general.AddKey(nameof(Name), _name);\n\n        if (_culture is not null)\n            general.AddKey(nameof(Culture), _culture.Name);\n\n        general.AddKey(nameof(Author), Author);\n\n        general.AddKey(nameof(MapEncoding), EncodingExt.EncodingWithAutoToString(MapEncoding));\n\n        ini.AddSection(nameof(Values));\n        IniSection translation = ini.GetSection(nameof(Values));\n\n        foreach (var (key, value) in Values.OrderBy(kvp => kvp.Key))\n        {\n            bool valueMissing = MissingKeys.Contains(key);\n            if (!saveOnlyMissingValues || valueMissing)\n            {\n                translation.AddKey(valueMissing\n                        ? MISSING_KEY_PREFIX + key\n                        : key,\n                    value.ToIniString());\n            }\n        }\n\n        return ini;\n    }\n\n    private bool HandleMissing(string key, string defaultValue)\n    {\n        if (MissingKeys.Add(key))\n        {\n            Values[key] = defaultValue;\n            return true;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// Looks up the translated value that corresponds to the given key.\n    /// </summary>\n    /// <param name=\"key\">The translation key (identifier).</param>\n    /// <param name=\"defaultValue\">The value to fall back to in case there's no translated value.</param>\n    /// <param name=\"notify\">Whether to add this key and value to the list of missing key-values.</param>\n    /// <returns>The translated value or a default value.</returns>\n    public string LookUp(string key, string defaultValue, bool notify = true)\n    {\n        if (Values.ContainsKey(key))\n            return Values[key];\n\n        if (notify)\n            _ = HandleMissing(key, defaultValue);\n\n        return defaultValue;\n    }\n\n    /// <summary>\n    /// Looks up the translated value that corresponds to the given key with a fallback to the value of global key.\n    /// </summary>\n    /// <param name=\"key\">The translation key (identifier).</param>\n    /// <param name=\"fallbackKey\">The fallback translation key (identifier).</param>\n    /// <param name=\"defaultValue\">The value to fall back to in case there's no translated value.</param>\n    /// <param name=\"notify\">Whether to add this key and value to the list of missing key-values. Doesn't include the fallback key.</param>\n    /// <returns>The translated value or a default value.</returns>\n    public string LookUp(string key, string fallbackKey, string defaultValue, bool notify = true)\n    {\n        string result;\n        if (Values.ContainsKey(key))\n        {\n            result = Values[key];\n        }\n        else if (key != fallbackKey && Values.ContainsKey(fallbackKey))\n        {\n            result = Values[fallbackKey];\n        }\n        else\n        {\n            result = defaultValue;\n\n            if (notify)\n                _ = HandleMissing(key, defaultValue);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "ClientCore/I18N/TranslationGameFile.cs",
    "content": "﻿namespace ClientCore.I18N;\n\n/// <summary>\n/// Describes a file to try and copy into the game folder with a translation.\n/// </summary>\n/// <param name=\"Source\">A path to copy from, relative to the selected translation folder.</param>\n/// <param name=\"Target\">A path to copy to, relative to root folder of the game/mod.</param>\n/// <param name=\"Checked\">Whether to include this file in the integrity checks.</param>\npublic readonly record struct TranslationGameFile(string Source, string Target, bool Checked);"
  },
  {
    "path": "ClientCore/INIProcessing/IniPreprocessInfoStore.cs",
    "content": "﻿using Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing ClientCore.Extensions;\n\nnamespace ClientCore.INIProcessing\n{\n    public class PreprocessedIniInfo\n    {\n        public PreprocessedIniInfo(string fileName, string originalHash, string processedHash)\n        {\n            FileName = fileName;\n            OriginalFileHash = originalHash;\n            ProcessedFileHash = processedHash;\n        }\n\n        public PreprocessedIniInfo(string[] info)\n        {\n            FileName = info[0];\n            OriginalFileHash = info[1];\n            ProcessedFileHash = info[2];\n        }\n\n        public string FileName { get; }\n        public string OriginalFileHash { get; set; }\n        public string ProcessedFileHash { get; set; }\n    }\n\n    /// <summary>\n    /// Handles information on what INI files have been processed by the client.\n    /// </summary>\n    public class IniPreprocessInfoStore\n    {\n        private const string StoreIniName = \"ProcessedIniInfo.ini\";\n        private const string ProcessedINIsSection = \"ProcessedINIs\";\n\n        public List<PreprocessedIniInfo> PreprocessedIniInfos { get; } = new List<PreprocessedIniInfo>();\n\n        /// <summary>\n        /// Loads the preprocessed INI information.\n        /// </summary>\n        public void Load()\n        {\n            FileInfo processedIniInfoFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, \"ProcessedIniInfo.ini\");\n\n            if (!processedIniInfoFile.Exists)\n                return;\n\n            var iniFile = new IniFile(processedIniInfoFile.FullName);\n            var keys = iniFile.GetSectionKeys(ProcessedINIsSection);\n            foreach (string key in keys)\n            {\n                string[] values = iniFile.GetStringListValue(ProcessedINIsSection, key, string.Empty);\n\n                if (values.Length != 3)\n                {\n                    Logger.Log(\"Failed to parse preprocessed INI info, key \" + key);\n                    continue;\n                }\n\n                // If an INI file no longer exists, it's useless to keep its record\n                if (!SafePath.GetFile(ProgramConstants.GamePath, \"INI\", values[0]).Exists)\n                    continue;\n\n                PreprocessedIniInfos.Add(new PreprocessedIniInfo(values));\n            }\n        }\n\n        /// <summary>\n        /// Checks if a (potentially processed) INI file is up-to-date \n        /// or whether it needs to be (re)processed.\n        /// </summary>\n        /// <param name=\"fileName\">The name of the INI file in its directory.\n        /// Do not supply the entire file path.</param>\n        /// <returns>True if the INI file is up-to-date, false if it needs to be processed.</returns>\n        public bool IsIniUpToDate(string fileName)\n        {\n            PreprocessedIniInfo info = PreprocessedIniInfos.Find(i => i.FileName == fileName);\n\n            if (info == null)\n                return false;\n\n            string processedFileHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", fileName));\n            if (processedFileHash != info.ProcessedFileHash)\n                return false;\n\n            string originalFileHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", \"Base\", fileName));\n            if (originalFileHash != info.OriginalFileHash)\n                return false;\n\n            return true;\n        }\n\n        public void UpsertRecord(string fileName, string originalFileHash, string processedFileHash)\n        {\n            var existing = PreprocessedIniInfos.Find(i => i.FileName == fileName);\n            if (existing == null)\n            {\n                PreprocessedIniInfos.Add(new PreprocessedIniInfo(fileName, originalFileHash, processedFileHash));\n            }\n            else\n            {\n                existing.OriginalFileHash = originalFileHash;\n                existing.ProcessedFileHash = processedFileHash;\n            }\n        }\n\n        public void Write()\n        {\n            FileInfo processedIniInfoFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, \"ProcessedIniInfo.ini\");\n\n            if (processedIniInfoFile.Exists)\n                processedIniInfoFile.Delete();\n\n            IniFile iniFile = new IniFile(processedIniInfoFile.FullName);\n            for (int i = 0; i < PreprocessedIniInfos.Count; i++)\n            {\n                PreprocessedIniInfo info = PreprocessedIniInfos[i];\n\n                iniFile.SetStringValue(ProcessedINIsSection, i.ToString(),\n                    string.Join(\",\", info.FileName, info.OriginalFileHash, info.ProcessedFileHash));\n            }\n            iniFile.WriteIniFile();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/INIProcessing/IniPreprocessor.cs",
    "content": "﻿using Rampastring.Tools;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace ClientCore.INIProcessing\n{\n    /// <summary>\n    /// Pre-processes INI files.\n    /// Allows sections to inherit other sections.\n    /// </summary>\n    public class IniPreprocessor\n    {\n        public void ProcessIni(string sourceIniPath, string destinationIniPath)\n        {\n            File.Delete(destinationIniPath);\n\n            if (!File.Exists(sourceIniPath))\n                return;\n\n            var iniFile = new IniFile(sourceIniPath);\n            List<string> sections = iniFile.GetSections();\n            sections.ForEach(sectionName => ProcessSection(iniFile, sectionName));\n            iniFile.Comment = $\"generated by CnCNet client, see /Base/{Path.GetFileName(sourceIniPath)} for the original\";\n\n            iniFile.WriteIniFile(destinationIniPath);\n        }\n\n        /// <summary>\n        /// Processes an INI section and applies its potential base section.\n        /// Returns the INI section. Works recursively.\n        /// </summary>\n        /// <param name=\"iniFile\">The INI file.</param>\n        /// <param name=\"sectionName\">The name of the section to process.</param>\n        private IniSection ProcessSection(IniFile iniFile, string sectionName)\n        {\n            IniSection section = iniFile.GetSection(sectionName);\n            if (section == null)\n                return null;\n\n            string baseSectionName = section.GetStringValue(\"BaseSection\", string.Empty);\n            if (string.IsNullOrWhiteSpace(baseSectionName))\n                return section;\n\n            IniSection baseSection = ProcessSection(iniFile, baseSectionName);\n            if (baseSection == null)\n                return section;\n\n            foreach (var kvp in baseSection.Keys)\n            {\n                if (!section.KeyExists(kvp.Key))\n                    section.AddKey(kvp.Key, kvp.Value);\n            }\n\n            return section;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/INIProcessing/PreprocessorBackgroundTask.cs",
    "content": "﻿using Rampastring.Tools;\nusing System.IO;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\n\nnamespace ClientCore.INIProcessing\n{\n    /// <summary>\n    /// Background task for pre-processing INI files.\n    /// Singleton.\n    /// </summary>\n    public class PreprocessorBackgroundTask\n    {\n        private PreprocessorBackgroundTask()\n        {\n        }\n\n        private static PreprocessorBackgroundTask _instance;\n        public static PreprocessorBackgroundTask Instance\n        {\n            get\n            {\n                if (_instance == null)\n                    _instance = new PreprocessorBackgroundTask();\n\n                return _instance;\n            }\n        }\n\n        private Task task;\n\n        public bool IsRunning => !task.IsCompleted;\n\n        public void Run()\n        {\n            task = Task.Run(CheckFiles);\n        }\n\n        private static void CheckFiles()\n        {\n            Logger.Log(\"Starting background processing of INI files.\");\n\n            DirectoryInfo iniFolder = SafePath.GetDirectory(ProgramConstants.GamePath, \"INI\", \"Base\");\n\n            if (!iniFolder.Exists)\n            {\n                Logger.Log(\"/INI/Base does not exist, skipping background processing of INI files.\");\n                return;\n            }\n\n            IniPreprocessInfoStore infoStore = new IniPreprocessInfoStore();\n            infoStore.Load();\n\n            IniPreprocessor processor = new IniPreprocessor();\n\n            IEnumerable<FileInfo> iniFiles = iniFolder.EnumerateFiles(\"*.ini\", SearchOption.TopDirectoryOnly);\n\n            int processedCount = 0;\n\n            foreach (FileInfo iniFile in iniFiles)\n            {\n                if (!infoStore.IsIniUpToDate(iniFile.Name))\n                {\n                    Logger.Log(\"INI file \" + iniFile.Name + \" is not processed or outdated, re-processing it.\");\n\n                    string sourcePath = iniFile.FullName;\n                    string destinationPath = SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", iniFile.Name);\n\n                    processor.ProcessIni(sourcePath, destinationPath);\n\n                    string sourceHash = Utilities.CalculateSHA1ForFile(sourcePath);\n                    string destinationHash = Utilities.CalculateSHA1ForFile(destinationPath);\n                    infoStore.UpsertRecord(iniFile.Name, sourceHash, destinationHash);\n                    processedCount++;\n                }\n                else\n                {\n                    Logger.Log(\"INI file \" + iniFile.Name + \" is up to date.\");\n                }\n            }\n\n            if (processedCount > 0)\n            {\n                Logger.Log(\"Writing preprocessed INI info store.\");\n                infoStore.Write();\n            }\n\n            Logger.Log(\"Ended background processing of INI files.\");\n        }\n    }\n}"
  },
  {
    "path": "ClientCore/LoadingScreenController.cs",
    "content": "﻿using System;\nusing Rampastring.Tools;\n\nnamespace ClientCore\n{\n    public static class LoadingScreenController\n    {\n        public static string GetLoadScreenName(string sideId)\n        {\n            int resHeight = UserINISettings.Instance.IngameScreenHeight;\n            int randomInt = new Random().Next(1, 1 + ClientConfiguration.Instance.LoadingScreenCount);\n            string resolutionText;\n\n            if (resHeight < 480)\n                resolutionText = \"400\";\n            else if (resHeight < 600)\n                resolutionText = \"480\";\n            else\n                resolutionText = \"600\";\n\n            return SafePath.CombineFilePath(\n                ProgramConstants.BASE_RESOURCE_PATH,\n                FormattableString.Invariant($\"l{resolutionText}s{sideId}{randomInt}.pcx\")).Replace('\\\\', '/');\n        }\n    }\n}"
  },
  {
    "path": "ClientCore/OSVersion.cs",
    "content": "﻿public enum OSVersion\n{\n    UNKNOWN,\n    WINXP,\n    WINVISTA,\n    WIN7,\n    WIN810,\n    UNIX\n}"
  },
  {
    "path": "ClientCore/PlatformShim/EncodingExt.cs",
    "content": "#nullable enable\nusing System.Text;\n\nnamespace ClientCore.PlatformShim;\n\npublic static class EncodingExt\n{\n    static EncodingExt()\n    {\n        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);\n\n        ANSI = Encoding.GetEncoding(0);\n    }\n\n    /// <summary>\n    /// Gets the legacy ANSI encoding (not Windows-1252 and also not any specific encoding).\n    /// ANSI doesn't mean a specific codepage, it means the default non-Unicode codepage which can be changed from Control Panel.\n    /// </summary>\n    public static Encoding ANSI { get; }\n\n    public static Encoding UTF8NoBOM { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);\n\n    public const string ENCODING_AUTO_DETECT = \"Auto\";\n\n    public static Encoding? GetEncodingWithAuto(string? encodingName)\n    {\n        if (encodingName is null)\n            return UTF8NoBOM;\n\n        if (encodingName.Equals(ENCODING_AUTO_DETECT, System.StringComparison.InvariantCultureIgnoreCase))\n            return null;\n\n        Encoding encoding = Encoding.GetEncoding(encodingName);\n\n        // We don't expect UTF-8 BOM for the string \"UTF-8\"\n        if (encoding is UTF8Encoding)\n            encoding = UTF8NoBOM;\n\n        return encoding;\n    }\n\n    public static string EncodingWithAutoToString(Encoding? encoding)\n    {\n        if (encoding is null)\n            return ENCODING_AUTO_DETECT;\n\n        // To find a name that can be passed to the GetDetectedEncoding method, use the WebName property.\n        return encoding.WebName;\n    }\n}"
  },
  {
    "path": "ClientCore/ProcessLauncher.cs",
    "content": "﻿using System.Diagnostics;\n\nnamespace ClientCore\n{\n    public static class ProcessLauncher\n    {\n        public static void StartShellProcess(string commandLine, string arguments = null)\n        {\n            using var _ = Process.Start(new ProcessStartInfo\n            {\n                FileName = commandLine,\n                Arguments = arguments,\n                UseShellExecute = true\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/ProfanityFilter.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text.RegularExpressions;\n\nnamespace ClientCore\n{\n    public class ProfanityFilter\n    {\n        public IList<string> CensoredWords { get; private set; }\n\n        /// <summary>\n        /// Creates a new profanity filter with a default set of censored words.\n        /// </summary>\n        public ProfanityFilter()\n        {\n            CensoredWords = new List<string>()\n            {\n                \"cunt*\",\n                \"*nigg*\",\n                \"paki*\",\n                \"shit\",\n                \"fuck*\",\n                \"admin*\",\n                \"allahu*\",\n                \"akbar\",\n                \"twat\",\n                \"cock\",\n                \"pussy\",\n                \"hitler*\",\n                \"anal\",\n                \"dick\",\n                \"faggot\",\n                \"whore\",\n                \"slut\",\n                \"motherfucker\",\n                \"asshole\",\n                \"bitch\",\n                \"bastard\",\n                \"kike\",\n                \"chink\",\n                \"spic\",\n                \"retard\",\n                \"tranny\",\n                \"jizz\",\n                \"gangbang\",\n                \"handjob\",\n                \"blowjob\",\n                \"rimjob\",\n                \"porn\",\n                \"rape\",\n                \"rapist\",\n                \"molest\",\n                \"incest\",\n                \"bestiality\",\n                \"zoophile\",\n                \"zoophilia\",\n                \"chingchong\",\n                \"slanty\",\n                \"zipperhead\",\n                \"gook\",\n            };\n        }\n\n        public ProfanityFilter(IEnumerable<string> censoredWords)\n        {\n            if (censoredWords == null)\n               throw new ArgumentNullException(nameof(censoredWords));\n            CensoredWords = new List<string>(censoredWords);\n        }\n\n        public bool IsOffensive(string text)\n        {\n            foreach (string censoredWord in CensoredWords)\n            {\n                string regularExpression = ToRegexPattern(censoredWord);\n                 if (Regex.IsMatch(text, regularExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))\n                    return true;\n            }\n            return false;\n        }\n\n        public string CensorText(string text)\n        {\n            if (text == null)\n                throw new ArgumentNullException(nameof(text));\n            string censoredText = text;\n            foreach (string censoredWord in CensoredWords)\n            {\n                string regularExpression = ToRegexPattern(censoredWord);\n                censoredText = Regex.Replace(censoredText, regularExpression, StarCensoredMatch,\n                  RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);\n            }\n            return censoredText;\n        }\n\n        private static string StarCensoredMatch(Match m)\n        {\n            string word = m.Captures[0].Value;\n            return new string('*', word.Length);\n        }\n\n        private string ToRegexPattern(string wildcardSearch)\n        {\n            string regexPattern = Regex.Escape(wildcardSearch);\n            regexPattern = regexPattern.Replace(@\"\\*\", \".*?\");\n            regexPattern = regexPattern.Replace(@\"\\?\", \".\");\n            if (regexPattern.StartsWith(\".*?\"))\n            {\n                regexPattern = regexPattern.Substring(3);\n                regexPattern = @\"(^\\b)*?\" + regexPattern;\n            }\n            regexPattern = @\"\\b\" + regexPattern + @\"\\b\";\n            return regexPattern;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/ProgramConstants.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Reflection;\nusing Rampastring.Tools;\nusing ClientCore.Extensions;\n\nnamespace ClientCore\n{\n    /// <summary>\n    /// Contains various static variables and constants that the client uses for operation.\n    /// </summary>\n    public static class ProgramConstants\n    {\n        public static readonly string StartupExecutable = Assembly.GetEntryAssembly().Location;\n\n        public static readonly string StartupPath = SafePath.CombineDirectoryPath(new FileInfo(StartupExecutable).Directory.FullName);\n\n        public static readonly string GamePath = SafePath.CombineDirectoryPath(GetGamePath(StartupPath));\n\n        public static string ClientUserFilesPath => SafePath.CombineDirectoryPath(GamePath, \"Client\");\n\n        public static event EventHandler PlayerNameChanged;\n\n        public const string QRES_EXECUTABLE = \"qres.dat\";\n\n        public const string CNCNET_PROTOCOL_REVISION = \"R14\";\n        public const string LAN_PROTOCOL_REVISION = \"RL8\";\n        public const int LAN_PORT = 1234;\n        public const int LAN_INGAME_PORT = 1234;\n        public const int LAN_LOBBY_PORT = 1232;\n        public const int LAN_GAME_LOBBY_PORT = 1233;\n        public const char LAN_DATA_SEPARATOR = (char)01;\n        public const char LAN_MESSAGE_SEPARATOR = (char)02;\n\n        public const string SPAWNMAP_INI = \"spawnmap.ini\";\n        public const string SPAWNER_SETTINGS = \"spawn.ini\";\n        public const string SAVED_GAME_SPAWN_INI = \"Saved Games/spawnSG.ini\";\n\n        /// <summary>\n        /// The locale code that corresponds to the language the hardcoded client strings are in.\n        /// </summary>\n        public const string HARDCODED_LOCALE_CODE = \"en\";\n\n        /// <summary>\n        /// Used to denote <see cref=\"Environment.NewLine\"/> in the INI files.\n        /// </summary>\n        /// <remarks>\n        /// Historically Westwood used '@' for this purpose, so we keep it for compatibility.\n        /// </remarks>\n        public const string INI_NEWLINE_PATTERN = \"@\";\n\n        public const int GAME_ID_MAX_LENGTH = 4;\n\n        public static readonly Encoding LAN_ENCODING = Encoding.UTF8;\n\n#if NETFRAMEWORK\n        private static bool? isMono;\n\n        /// <summary>\n        /// Gets a value whether or not the application is running under Mono. Uses lazy loading and caching.\n        /// </summary>\n        public static bool ISMONO => isMono ??= Type.GetType(\"Mono.Runtime\") != null;\n#endif\n\n        public static string GAME_VERSION = \"Undefined\";\n        private static string PlayerName = \"No name\";\n\n        public static string PLAYERNAME\n        {\n            get { return PlayerName; }\n            set\n            {\n                string oldPlayerName = PlayerName;\n                PlayerName = value;\n                if (oldPlayerName != PlayerName)\n                    PlayerNameChanged?.Invoke(null, EventArgs.Empty);\n            }\n        }\n\n        public static string BASE_RESOURCE_PATH = \"Resources\";\n        public static string RESOURCES_DIR = BASE_RESOURCE_PATH;\n\n        public static int LOG_LEVEL = 1;\n\n        public static bool IsInGame { get; set; }\n\n        public static string GetResourcePath()\n        {\n            return SafePath.CombineDirectoryPath(GamePath, RESOURCES_DIR);\n        }\n\n        public static string GetBaseResourcePath()\n        {\n            return SafePath.CombineDirectoryPath(GamePath, BASE_RESOURCE_PATH);\n        }\n\n        public const string GAME_INVITE_CTCP_COMMAND = \"INVITE\";\n        public const string GAME_INVITATION_FAILED_CTCP_COMMAND = \"INVITATION_FAILED\";\n\n        public static string GetAILevelName(int aiLevel)\n        {\n            if (aiLevel > -1 && aiLevel < AI_PLAYER_NAMES.Count)\n                return AI_PLAYER_NAMES[aiLevel];\n\n            return \"\";\n        }\n\n        public static readonly List<string> TEAMS = new List<string> { \"A\", \"B\", \"C\", \"D\" };\n\n        // Static fields might be initialized before the translation file is loaded. Change to readonly properties here.\n        public static List<string> AI_PLAYER_NAMES => new List<string> { \"Easy AI\".L10N(\"Client:Main:EasyAIName\"), \"Medium AI\".L10N(\"Client:Main:MediumAIName\"), \"Hard AI\".L10N(\"Client:Main:HardAIName\") };\n\n        public static string LogFileName { get; set; }\n\n        /// <summary>\n        /// This method finds the \"Resources\" directory by traversing the directory tree upwards from the startup path.\n        /// </summary>\n        /// <remarks>\n        /// This method is needed by both ClientCore and DXMainClient. However, since it is usually called at the very beginning,\n        /// where DXMainClient could not refer to ClientCore, this method is copied to both projects.\n        /// Remember to keep <see cref=\"ClientCore.ProgramConstants.SearchResourcesDir\"/> and <see cref=\"DTAClient.Program.SearchResourcesDir\"/> consistent if you have modified its source codes.\n        /// </remarks>\n        private static string SearchResourcesDir(string startupPath)\n        {\n            DirectoryInfo currentDir = new(startupPath);\n            for (int i = 0; i < 3; i++)\n            {\n                // Determine if currentDir is the \"Resources\" folder\n                if (currentDir.Name.ToLowerInvariant() == \"Resources\".ToLowerInvariant())\n                    return currentDir.FullName;\n\n                // Additional check. This makes developers to debug the client inside Visual Studio a little bit easier.\n                DirectoryInfo resourcesDir = currentDir.GetDirectories(\"Resources\", SearchOption.TopDirectoryOnly).FirstOrDefault();\n                if (resourcesDir is not null)\n                    return resourcesDir.FullName;\n\n                currentDir = currentDir.Parent;\n            }\n\n            throw new Exception(\"Could not find Resources directory.\");\n        }\n\n        private static string GetGamePath(string startupPath)\n        {\n            string resourceDir = SearchResourcesDir(startupPath);\n            return new DirectoryInfo(resourceDir).Parent.FullName;\n        }\n    }\n}"
  },
  {
    "path": "ClientCore/SavedGameManager.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing Rampastring.Tools;\n\nnamespace ClientCore\n{\n    /// <summary>\n    /// A class for handling saved multiplayer games.\n    /// </summary>\n    public static class SavedGameManager\n    {\n        private const string SAVED_GAMES_DIRECTORY = \"Saved Games\";\n\n        private static bool saveRenameInProgress = false;\n\n        public static int GetSaveGameCount()\n        {\n            string saveGameDirectory = GetSaveGameDirectoryPath();\n\n            if (!AreSavedGamesAvailable())\n                return 0;\n\n            for (int i = 0; i < 1000; i++)\n            {\n                if (!SafePath.GetFile(saveGameDirectory, string.Format(\"SVGM_{0}.NET\", i.ToString(\"D3\"))).Exists)\n                {\n                    return i;\n                }\n            }\n\n            return 1000;\n        }\n\n        public static List<string> GetSaveGameTimestamps()\n        {\n            int saveGameCount = GetSaveGameCount();\n\n            List<string> timestamps = new List<string>();\n\n            string saveGameDirectory = GetSaveGameDirectoryPath();\n\n            for (int i = 0; i < saveGameCount; i++)\n            {\n                FileInfo sgFile = SafePath.GetFile(saveGameDirectory, string.Format(\"SVGM_{0}.NET\", i.ToString(\"D3\")));\n\n                DateTime dt = sgFile.LastWriteTime;\n\n                timestamps.Add(dt.ToString());\n            }\n\n            return timestamps;\n        }\n\n        public static bool AreSavedGamesAvailable()\n        {\n            if (Directory.Exists(GetSaveGameDirectoryPath()))\n                return true;\n\n            return false;\n        }\n\n        private static string GetSaveGameDirectoryPath()\n        {\n            return SafePath.CombineDirectoryPath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY);\n        }\n\n        /// <summary>\n        /// Initializes saved MP games for a match.\n        /// </summary>\n        public static bool InitSavedGames()\n        {\n            bool success = EraseSavedGames();\n\n            if (!success)\n                return false;\n\n            try\n            {\n                Logger.Log(\"Writing spawn.ini for saved game.\");\n                SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, \"spawnSG.ini\");\n                File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, \"spawn.ini\"), SafePath.CombineFilePath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, \"spawnSG.ini\"));\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Writing spawn.ini for saved game failed! Exception message: \" + ex.ToString());\n                return false;\n            }\n\n            return true;\n        }\n\n        public static void RenameSavedGame()\n        {\n            Logger.Log(\"Renaming saved game.\");\n\n            if (saveRenameInProgress)\n            {\n                Logger.Log(\"Save renaming in progress!\");\n                return;\n            }\n\n            string saveGameDirectory = GetSaveGameDirectoryPath();\n\n            if (!SafePath.GetFile(saveGameDirectory, \"SAVEGAME.NET\").Exists)\n            {\n                Logger.Log(\"SAVEGAME.NET doesn't exist!\");\n                return;\n            }\n\n            saveRenameInProgress = true;\n\n            int saveGameId = 0;\n\n            for (int i = 0; i < 1000; i++)\n            {\n                if (!SafePath.GetFile(saveGameDirectory, string.Format(\"SVGM_{0}.NET\", i.ToString(\"D3\"))).Exists)\n                {\n                    saveGameId = i;\n                    break;\n                }\n            }\n\n            if (saveGameId == 999)\n            {\n                if (SafePath.GetFile(saveGameDirectory, \"SVGM_999.NET\").Exists)\n                    Logger.Log(\"1000 saved games exceeded! Overwriting previous MP save.\");\n            }\n\n            string sgPath = SafePath.CombineFilePath(saveGameDirectory, string.Format(\"SVGM_{0}.NET\", saveGameId.ToString(\"D3\")));\n\n            int tryCount = 0;\n\n            while (true)\n            {\n                try\n                {\n                    File.Move(SafePath.CombineFilePath(saveGameDirectory, \"SAVEGAME.NET\"), sgPath);\n                    break;\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Renaming saved game failed! Exception message: \" + ex.ToString());\n                }\n\n                tryCount++;\n\n                if (tryCount > 40)\n                {\n                    Logger.Log(\"Renaming saved game failed 40 times! Aborting.\");\n                    return;\n                }\n\n                System.Threading.Thread.Sleep(250);\n            }\n\n            saveRenameInProgress = false;\n\n            Logger.Log(\"Saved game SAVEGAME.NET succesfully renamed to \" + Path.GetFileName(sgPath));\n        }\n\n        public static bool EraseSavedGames()\n        {\n            Logger.Log(\"Erasing previous MP saved games.\");\n\n            try\n            {\n                for (int i = 0; i < 1000; i++)\n                {\n                    SafePath.DeleteFileIfExists(GetSaveGameDirectoryPath(), string.Format(\"SVGM_{0}.NET\", i.ToString(\"D3\")));\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Erasing previous MP saved games failed! Exception message: \" + ex.ToString());\n                return false;\n            }\n\n            Logger.Log(\"MP saved games succesfully erased.\");\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/BoolSetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    public class BoolSetting : INISetting<bool>\n    {\n        public BoolSetting(IniFile iniFile, string iniSection, string iniKey, bool defaultValue)\n            : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n        }\n\n        protected override bool Get()\n        {\n            return IniFile.GetBooleanValue(IniSection, IniKey, DefaultValue);\n        }\n\n        protected override void Set(bool value)\n        {\n            IniFile.SetBooleanValue(IniSection, IniKey, value);\n        }\n\n        public override void Write()\n        {\n            IniFile.SetBooleanValue(IniSection, IniKey, Get());\n        }\n\n        public override string ToString()\n        {\n            return Get().ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/DoubleSetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    public class DoubleSetting : INISetting<double>\n    {\n        public DoubleSetting(IniFile iniFile, string iniSection, string iniKey, double defaultValue)\n            : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n        }\n\n        protected override double Get()\n        {\n            return IniFile.GetDoubleValue(IniSection, IniKey, DefaultValue);\n        }\n\n        protected override void Set(double value)\n        {\n            IniFile.SetDoubleValue(IniSection, IniKey, value);\n        }\n\n        public override void Write()\n        {\n            IniFile.SetDoubleValue(IniSection, IniKey, Get());\n        }\n\n        public override string ToString()\n        {\n            return Get().ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/IIniSetting.cs",
    "content": "﻿namespace ClientCore.Settings\n{\n    /// <summary>\n    /// A dummy interface for checking for INISetting in reflection.\n    /// </summary>\n    interface IIniSetting\n    {\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/INISetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    /// <summary>\n    /// A base class for an INI setting.\n    /// </summary>\n    public abstract class INISetting<T> : IIniSetting\n    {\n        public INISetting(IniFile iniFile, string iniSection, string iniKey,\n            T defaultValue)\n        {\n            IniFile = iniFile;\n            IniSection = iniSection;\n            IniKey = iniKey;\n            DefaultValue = defaultValue;\n        }\n\n        public static implicit operator T(INISetting<T> iniSetting)\n        {\n            return iniSetting.Get();\n        }\n\n        public void SetIniFile(IniFile iniFile)\n        {\n            IniFile = iniFile;\n        }\n\n        protected IniFile IniFile { get; private set; }\n        protected string IniSection { get; private set; }\n        protected string IniKey { get; private set; }\n        protected T DefaultValue { get; private set; }\n\n        public T Value\n        {\n            get { return Get(); }\n            set { Set(value); }\n        }\n\n        /// <summary>\n        /// Writes the default value of this setting to the INI file if no value\n        /// for the setting is currently specified in the INI file.\n        /// </summary>\n        public void SetDefaultIfNonexistent()\n        {\n            if (!IniFile.KeyExists(IniSection, IniKey))\n                Set(DefaultValue);\n        }\n\n        protected abstract T Get();\n\n        protected abstract void Set(T value);\n\n        public abstract void Write();\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/IntRangeSetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    /// <summary>\n    /// Similar to IntSetting, this setting forces a min and max value upon getting and setting.\n    /// </summary>\n    public class IntRangeSetting : IntSetting\n    {\n        private readonly int MinValue;\n        private readonly int MaxValue;\n\n        public IntRangeSetting(IniFile iniFile, string iniSection, string iniKey, int defaultValue, int minValue, int maxValue) : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n            MinValue = minValue;\n            MaxValue = maxValue;\n        }\n\n        /// <summary>\n        /// Checks the validity of the value. If the value is invalid, return the default value of this setting.\n        /// Otherwise, return the set value.\n        /// </summary>\n        /// <param name=\"value\"></param>\n        /// <returns></returns>\n        private int NormalizeValue(int value)\n        {\n            return InvalidValue(value) ? DefaultValue : value;\n        }\n\n        private bool InvalidValue(int value)\n        {\n            return value < MinValue || value > MaxValue;\n        }\n\n        protected override int Get()\n        {\n            return NormalizeValue(IniFile.GetIntValue(IniSection, IniKey, DefaultValue));\n        }\n\n        protected override void Set(int value)\n        {\n            IniFile.SetIntValue(IniSection, IniKey, NormalizeValue(value));\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/IntSetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    public class IntSetting : INISetting<int>\n    {\n        public IntSetting(IniFile iniFile, string iniSection, string iniKey, int defaultValue)\n            : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n        }\n\n        protected override int Get()\n        {\n            return IniFile.GetIntValue(IniSection, IniKey, DefaultValue);\n        }\n\n        protected override void Set(int value)\n        {\n            IniFile.SetIntValue(IniSection, IniKey, value);\n        }\n\n        public override void Write()\n        {\n            IniFile.SetIntValue(IniSection, IniKey, Get());\n        }\n\n        public override string ToString()\n        {\n            return Get().ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/StringListSetting.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    /// <summary>\n    /// This is a setting that can be stored as a comma separated list of strings.\n    /// </summary>\n    public class StringListSetting : INISetting<List<string>>\n    {\n        public StringListSetting(IniFile iniFile, string iniSection, string iniKey, List<string> defaultValue) : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n        }\n\n        protected override List<string> Get()\n        {\n            string value = IniFile.GetStringValue(IniSection, IniKey, \"\");\n            return string.IsNullOrWhiteSpace(value) ? DefaultValue : value.Split(',').ToList();\n        }\n\n        protected override void Set(List<string> value)\n        {\n            IniFile.SetStringValue(IniSection, IniKey, string.Join(\",\", value));\n        }\n\n        public override void Write()\n        {\n            IniFile.SetStringValue(IniSection, IniKey, string.Join(\",\", Get()));\n        }\n\n        public void Add(string value)\n        {\n            var values = Get().Concat(new []{value}).ToList();\n            Set(values);\n        }\n\n        public void Remove(string value)\n        {\n            var values = Get().Where(v => !string.Equals(v, value, StringComparison.InvariantCultureIgnoreCase)).ToList();\n            Set(values);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/StringSetting.cs",
    "content": "﻿using Rampastring.Tools;\n\nnamespace ClientCore.Settings\n{\n    public class StringSetting : INISetting<string>\n    {\n        public StringSetting(IniFile iniFile, string iniSection, string iniKey, string defaultValue)\n            : base(iniFile, iniSection, iniKey, defaultValue)\n        {\n        }\n\n        protected override string Get()\n        {\n            return IniFile.GetStringValue(IniSection, IniKey, DefaultValue);\n        }\n\n        protected override void Set(string value)\n        {\n            IniFile.SetStringValue(IniSection, IniKey, value);\n        }\n\n        public override void Write()\n        {\n            IniFile.SetStringValue(IniSection, IniKey, Get());\n        }\n\n        public override string ToString()\n        {\n            return Get();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Settings/UserINISettings.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\nusing ClientCore.Settings;\n\nusing Rampastring.Tools;\n\nnamespace ClientCore\n{\n    public class UserINISettings\n    {\n        private static UserINISettings _instance;\n\n        public const string VIDEO = \"Video\";\n        public const string MULTIPLAYER = \"MultiPlayer\";\n        public const string OPTIONS = \"Options\";\n        public const string AUDIO = \"Audio\";\n        public const string COMPATIBILITY = \"Compatibility\";\n        public const string GAME_FILTERS = \"GameFilters\";\n        public const string GAME_OPTION_FILTERS = \"GameOptionFilters\";\n        private const string FAVORITE_MAPS = \"FavoriteMaps\";\n\n        private const bool DEFAULT_SHOW_FRIENDS_ONLY_GAMES = false;\n        private const bool DEFAULT_HIDE_LOCKED_GAMES = false;\n        private const bool DEFAULT_HIDE_PASSWORDED_GAMES = false;\n        private const bool DEFAULT_HIDE_INCOMPATIBLE_GAMES = false;\n        private const int DEFAULT_MAX_PLAYER_COUNT = 8;\n\n        public static UserINISettings Instance\n        {\n            get\n            {\n                if (_instance == null)\n                    throw new InvalidOperationException(\"UserINISettings not initialized!\");\n\n                return _instance;\n            }\n        }\n\n        public static void Initialize(string userIniFileName)\n        {\n            if (_instance != null)\n                throw new InvalidOperationException(\"UserINISettings has already been initialized!\");\n\n            var userIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, userIniFileName));\n\n            string userDefaultIniFilePath = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), \"UserDefaults.ini\");\n\n            if (!File.Exists(userDefaultIniFilePath))\n            {\n                _instance = new UserINISettings(userIni);\n                return;\n            }\n\n            var userDefaultIni = new IniFile(userDefaultIniFilePath);\n\n            var combinedUserIni = userDefaultIni.Clone();\n            combinedUserIni.FileName = null;\n\n            // Combine userIni and userDefaultIni\n            foreach (string sectionName in userIni.GetSections())\n            {\n                IniSection userSection = userIni.GetSection(sectionName);\n\n                IniSection combinedUserSection = combinedUserIni.GetSection(sectionName);\n                if (combinedUserSection == null)\n                {\n                    combinedUserSection = new IniSection(sectionName);\n                    combinedUserIni.AddSection(combinedUserSection);\n                }\n\n                foreach ((var key, var value) in userSection.Keys)\n                {\n                    combinedUserSection.AddOrReplaceKey(key, value);\n                }\n            }\n\n            combinedUserIni.FileName = userIni.FileName;\n\n            _instance = new UserINISettings(combinedUserIni);\n        }\n\n        protected UserINISettings(IniFile iniFile)\n        {\n            SettingsIni = iniFile;\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n                BackBufferInVRAM = new BoolSetting(iniFile, VIDEO, \"UseGraphicsPatch\", true);\n            else\n                BackBufferInVRAM = new BoolSetting(iniFile, VIDEO, \"VideoBackBuffer\", false);\n\n            IngameScreenWidth = new IntSetting(\n                iniFile,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? \"Width\" : \"ScreenWidth\",\n                1024);\n\n            IngameScreenHeight = new IntSetting(\n                iniFile,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? \"Height\" : \"ScreenHeight\",\n                768);\n\n            ClientTheme = new StringSetting(iniFile, MULTIPLAYER, \"Theme\", ClientConfiguration.Instance.GetThemeInfoFromIndex(0).Name);\n            Translation = new StringSetting(iniFile, OPTIONS, \"Translation\", I18N.Translation.GetDefaultTranslationLocaleCode());\n            TranslationGameFilesVersion = new StringSetting(iniFile, OPTIONS, nameof(TranslationGameFilesVersion), string.Empty);\n\n            DetailLevel = new IntSetting(iniFile, OPTIONS, \"DetailLevel\", 2);\n            Renderer = new StringSetting(iniFile, COMPATIBILITY, \"Renderer\", string.Empty);\n            WindowedMode = new BoolSetting(iniFile, VIDEO, ClientConfiguration.Instance.WindowedModeKey, false);\n            BorderlessWindowedMode = new BoolSetting(iniFile, VIDEO, \"NoWindowFrame\", false);\n            BorderlessWindowedClient = new BoolSetting(iniFile, VIDEO, \"BorderlessWindowedClient\", ClientConfiguration.Instance.UserDefault_BorderlessWindowedClient);\n            IntegerScaledClient = new BoolSetting(iniFile, VIDEO, \"IntegerScaledClient\", ClientConfiguration.Instance.UserDefault_IntegerScaledClient);\n            ClientFPS = new IntSetting(iniFile, VIDEO, \"ClientFPS\", 60);\n            DisplayToggleableExtraTextures = new BoolSetting(iniFile, VIDEO, \"DisplayToggleableExtraTextures\", true);\n\n            // RA1 reads MultiplayerScoreVolume instead of ScoreVolume. This value is handled when saving\n            ScoreVolume = new DoubleSetting(iniFile,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,\n                \"ScoreVolume\",\n                0.7);\n\n            SoundVolume = new DoubleSetting(iniFile,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,\n                ClientConfiguration.Instance.ClientGameType == ClientType.RA ? \"Volume\" : \"SoundVolume\",\n                0.7);\n\n            VoiceVolume = new DoubleSetting(iniFile, AUDIO, \"VoiceVolume\", 0.7);\n            IsScoreShuffle = new BoolSetting(iniFile, AUDIO, \"IsScoreShuffle\", true);\n            ClientVolume = new DoubleSetting(iniFile, AUDIO, \"ClientVolume\", 1.0);\n            PlayMainMenuMusic = new BoolSetting(iniFile, AUDIO, \"PlayMainMenuMusic\", true);\n            StopMusicOnMenu = new BoolSetting(iniFile, AUDIO, \"StopMusicOnMenu\", true);\n            StopGameLobbyMessageAudio = new BoolSetting(iniFile, AUDIO, \"StopGameLobbyMessageAudio\", true);\n            MessageSound = new BoolSetting(iniFile, AUDIO, \"ChatMessageSound\", true);\n\n            ScrollRate = new IntSetting(iniFile, OPTIONS, \"ScrollRate\", 3);\n            DragDistance = new IntSetting(iniFile, OPTIONS, \"DragDistance\", 4);\n            CustomDragDistance = new IntSetting(iniFile, OPTIONS, \"CustomDragDistance\", 0);\n            DoubleTapInterval = new IntSetting(iniFile, OPTIONS, \"DoubleTapInterval\", 30);\n            Win8CompatMode = new StringSetting(iniFile, OPTIONS, \"Win8Compat\", \"No\");\n\n            PlayerName = new StringSetting(iniFile, MULTIPLAYER, \"Handle\", string.Empty);\n\n            ChatColor = new IntSetting(iniFile, MULTIPLAYER, \"ChatColor\", -1);\n            LANChatColor = new IntSetting(iniFile, MULTIPLAYER, \"LANChatColor\", -1);\n            PingUnofficialCnCNetTunnels = new BoolSetting(iniFile, MULTIPLAYER, \"PingCustomTunnels\", true);\n            WritePathToRegistry = new BoolSetting(iniFile, OPTIONS, \"WriteInstallationPathToRegistry\", ClientConfiguration.Instance.UserDefault_WriteInstallationPathToRegistry);\n            PlaySoundOnGameHosted = new BoolSetting(iniFile, MULTIPLAYER, \"PlaySoundOnGameHosted\", true);\n            SkipConnectDialog = new BoolSetting(iniFile, MULTIPLAYER, \"SkipConnectDialog\", false);\n            PersistentMode = new BoolSetting(iniFile, MULTIPLAYER, \"PersistentMode\", false);\n            AutomaticCnCNetLogin = new BoolSetting(iniFile, MULTIPLAYER, \"AutomaticCnCNetLogin\", false);\n            DiscordIntegration = new BoolSetting(iniFile, MULTIPLAYER, \"DiscordIntegration\", true);\n            SteamIntegration = new BoolSetting(iniFile, MULTIPLAYER, \"SteamIntegration\", true);\n            AllowGameInvitesFromFriendsOnly = new BoolSetting(iniFile, MULTIPLAYER, \"AllowGameInvitesFromFriendsOnly\", false);\n            NotifyOnUserListChange = new BoolSetting(iniFile, MULTIPLAYER, \"NotifyOnUserListChange\", true);\n            DisablePrivateMessagePopups = new BoolSetting(iniFile, MULTIPLAYER, \"DisablePrivateMessagePopups\", false);\n            AllowPrivateMessagesFromState = new IntSetting(iniFile, MULTIPLAYER, \"AllowPrivateMessagesFromState\", (int)AllowPrivateMessagesFromEnum.All);\n            EnableMapSharing = new BoolSetting(iniFile, MULTIPLAYER, \"EnableMapSharing\", true);\n            AlwaysDisplayTunnelList = new BoolSetting(iniFile, MULTIPLAYER, \"AlwaysDisplayTunnelList\", false);\n            MapSortState = new IntSetting(iniFile, MULTIPLAYER, \"MapSortState\", (int)SortDirection.None);\n            SearchAllGameModes = new BoolSetting(iniFile, MULTIPLAYER, \"SearchAllGameModes\", false);\n\n            CheckForUpdates = new BoolSetting(iniFile, OPTIONS, \"CheckforUpdates\", true);\n\n            PrivacyPolicyAccepted = new BoolSetting(iniFile, OPTIONS, \"PrivacyPolicyAccepted\", false);\n            IsFirstRun = new BoolSetting(iniFile, OPTIONS, \"IsFirstRun\", true);\n            CustomComponentsDenied = new BoolSetting(iniFile, OPTIONS, \"CustomComponentsDenied\", false);\n            Difficulty = new IntSetting(iniFile, OPTIONS, \"Difficulty\", 1);\n            ScrollDelay = new IntSetting(iniFile, OPTIONS, \"ScrollDelay\", 4);\n            GameSpeed = new IntSetting(iniFile, OPTIONS, \"GameSpeed\", 1);\n            ForceLowestDetailLevel = new BoolSetting(iniFile, VIDEO, \"ForceLowestDetailLevel\", false);\n            MinimizeWindowsOnGameStart = new BoolSetting(iniFile, OPTIONS, \"MinimizeWindowsOnGameStart\", true);\n            AutoRemoveUnderscoresFromName = new BoolSetting(iniFile, OPTIONS, \"AutoRemoveUnderscoresFromName\", true);\n            GenerateTranslationStub = new BoolSetting(iniFile, OPTIONS, nameof(GenerateTranslationStub), false);\n            GenerateOnlyNewValuesInTranslationStub = new BoolSetting(iniFile, OPTIONS, nameof(GenerateOnlyNewValuesInTranslationStub), false);\n\n            SortState = new IntSetting(iniFile, GAME_FILTERS, \"SortState\", (int)SortDirection.None);\n            ShowFriendGamesOnly = new BoolSetting(iniFile, GAME_FILTERS, \"ShowFriendGamesOnly\", DEFAULT_SHOW_FRIENDS_ONLY_GAMES);\n            HideLockedGames = new BoolSetting(iniFile, GAME_FILTERS, \"HideLockedGames\", DEFAULT_HIDE_LOCKED_GAMES);\n            HidePasswordedGames = new BoolSetting(iniFile, GAME_FILTERS, \"HidePasswordedGames\", DEFAULT_HIDE_PASSWORDED_GAMES);\n            HideIncompatibleGames = new BoolSetting(iniFile, GAME_FILTERS, \"HideIncompatibleGames\", DEFAULT_HIDE_INCOMPATIBLE_GAMES);\n            MaxPlayerCount = new IntRangeSetting(iniFile, GAME_FILTERS, \"MaxPlayerCount\", DEFAULT_MAX_PLAYER_COUNT, 2, 8);\n\n            LoadFavoriteMaps(iniFile);\n        }\n\n        public IniFile SettingsIni { get; private set; }\n\n        public event EventHandler SettingsSaved;\n\n        /*********/\n        /* VIDEO */\n        /*********/\n\n        public IntSetting IngameScreenWidth { get; private set; }\n        public IntSetting IngameScreenHeight { get; private set; }\n\n        public StringSetting ClientTheme { get; private set; }\n        public string ThemeFolderPath => ClientConfiguration.Instance.GetThemePath(ClientTheme);\n        public StringSetting Translation { get; private set; }\n        public StringSetting TranslationGameFilesVersion { get; private set; }\n        public string TranslationFolderPath => SafePath.CombineDirectoryPath(\n            ClientConfiguration.Instance.TranslationsFolderPath, Translation);\n        public string TranslationThemeFolderPath => SafePath.CombineDirectoryPath(\n            ClientConfiguration.Instance.TranslationsFolderPath, Translation,\n            ClientConfiguration.Instance.GetThemePath(ClientTheme));\n        public IntSetting DetailLevel { get; private set; }\n        public StringSetting Renderer { get; private set; }\n        public BoolSetting WindowedMode { get; private set; }\n        public BoolSetting BorderlessWindowedMode { get; private set; }\n        public BoolSetting BackBufferInVRAM { get; private set; }\n        public IntSetting ClientResolutionX { get; set; }\n        public IntSetting ClientResolutionY { get; set; }\n        public BoolSetting BorderlessWindowedClient { get; private set; }\n        public BoolSetting IntegerScaledClient { get; private set; }\n        public IntSetting ClientFPS { get; private set; }\n        public BoolSetting DisplayToggleableExtraTextures { get; private set; }\n\n        /*********/\n        /* AUDIO */\n        /*********/\n\n        public DoubleSetting ScoreVolume { get; private set; }\n        public DoubleSetting SoundVolume { get; private set; }\n        public DoubleSetting VoiceVolume { get; private set; }\n        public BoolSetting IsScoreShuffle { get; private set; }\n        public DoubleSetting ClientVolume { get; private set; }\n        public BoolSetting PlayMainMenuMusic { get; private set; }\n        public BoolSetting StopMusicOnMenu { get; private set; }\n        public BoolSetting StopGameLobbyMessageAudio { get; private set; }\n        public BoolSetting MessageSound { get; private set; }\n\n        /********/\n        /* GAME */\n        /********/\n\n        public IntSetting ScrollRate { get; private set; }\n        public IntSetting DragDistance { get; private set; }\n        // When > 0, overrides the auto-scaled DragDistance. Allows players to set a fixed pixel threshold regardless of resolution.\n        public IntSetting CustomDragDistance { get; private set; }\n        public IntSetting DoubleTapInterval { get; private set; }\n        public StringSetting Win8CompatMode { get; private set; }\n\n        /************************/\n        /* MULTIPLAYER (CnCNet) */\n        /************************/\n\n        public StringSetting PlayerName { get; private set; }\n\n        public IntSetting ChatColor { get; private set; }\n        public IntSetting LANChatColor { get; private set; }\n        public BoolSetting PingUnofficialCnCNetTunnels { get; private set; }\n        public BoolSetting WritePathToRegistry { get; private set; }\n        public BoolSetting PlaySoundOnGameHosted { get; private set; }\n\n        public BoolSetting SkipConnectDialog { get; private set; }\n        public BoolSetting PersistentMode { get; private set; }\n        public BoolSetting AutomaticCnCNetLogin { get; private set; }\n        public BoolSetting DiscordIntegration { get; private set; }\n        public BoolSetting SteamIntegration { get; private set; }\n        public BoolSetting AllowGameInvitesFromFriendsOnly { get; private set; }\n\n        public BoolSetting NotifyOnUserListChange { get; private set; }\n\n        public BoolSetting DisablePrivateMessagePopups { get; private set; }\n\n        public IntSetting AllowPrivateMessagesFromState { get; private set; }\n\n        public BoolSetting EnableMapSharing { get; private set; }\n\n        public BoolSetting AlwaysDisplayTunnelList { get; private set; }\n\n        public IntSetting MapSortState { get; private set; }\n\n        public BoolSetting SearchAllGameModes { get; private set; }\n\n        /*********************/\n        /* GAME LIST FILTERS */\n        /*********************/\n\n        public IntSetting SortState { get; private set; }\n\n        public BoolSetting ShowFriendGamesOnly { get; private set; }\n\n        public BoolSetting HideLockedGames { get; private set; }\n\n        public BoolSetting HidePasswordedGames { get; private set; }\n\n        public BoolSetting HideIncompatibleGames { get; private set; }\n\n        public IntRangeSetting MaxPlayerCount { get; private set; }\n\n        /************************/\n        /* GAME OPTION FILTERS */\n        /************************/\n\n        /// <summary>\n        /// Gets the filter value for a game option (checkbox or dropdown).\n        /// Returns null for \"All\" (no filter), or the selected index.\n        /// For checkboxes: 0 = Off, 1 = On.\n        /// For dropdowns: 0+ = actual option index.\n        /// </summary>\n        public int? GetGameOptionFilterValue(string optionName)\n        {\n            var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);\n            if (section == null || !section.KeyExists(optionName))\n                return null;\n\n            return section.GetIntValue(optionName, 0);\n        }\n\n        /// <summary>\n        /// Sets the filter value for a game option.\n        /// null = \"All\" (no filter), or the selected index.\n        /// When null, removes the key from INI. Otherwise stores the index value.\n        /// For checkboxes: 0 = Off, 1 = On.\n        /// For dropdowns: 0+ = actual option index.\n        /// </summary>\n        public void SetGameOptionFilterValue(string optionName, int? value)\n        {\n            if (value == null)\n                SettingsIni.GetSection(GAME_OPTION_FILTERS)?.RemoveKey(optionName);\n            else\n                SettingsIni.SetIntValue(GAME_OPTION_FILTERS, optionName, value.Value);\n        }\n\n        /********/\n        /* MISC */\n        /********/\n\n        public BoolSetting CheckForUpdates { get; private set; }\n\n        public BoolSetting PrivacyPolicyAccepted { get; private set; }\n        public BoolSetting IsFirstRun { get; private set; }\n        public BoolSetting CustomComponentsDenied { get; private set; }\n\n        public IntSetting Difficulty { get; private set; }\n\n        public IntSetting GameSpeed { get; private set; }\n\n        public IntSetting ScrollDelay { get; private set; }\n\n        public BoolSetting ForceLowestDetailLevel { get; private set; }\n\n        public BoolSetting MinimizeWindowsOnGameStart { get; private set; }\n\n        public BoolSetting AutoRemoveUnderscoresFromName { get; private set; }\n\n        public BoolSetting GenerateTranslationStub { get; private set; }\n\n        public BoolSetting GenerateOnlyNewValuesInTranslationStub { get; private set; }\n\n        public List<string> FavoriteMaps { get; private set; }\n\n        public void SetValue(string section, string key, string value)\n               => SettingsIni.SetStringValue(section, key, value);\n\n        public void SetValue(string section, string key, bool value)\n            => SettingsIni.SetBooleanValue(section, key, value);\n\n        public void SetValue(string section, string key, int value)\n            => SettingsIni.SetIntValue(section, key, value);\n\n        public string GetValue(string section, string key, string defaultValue)\n            => SettingsIni.GetStringValue(section, key, defaultValue);\n\n        public bool GetValue(string section, string key, bool defaultValue)\n            => SettingsIni.GetBooleanValue(section, key, defaultValue);\n\n        public int GetValue(string section, string key, int defaultValue)\n            => SettingsIni.GetIntValue(section, key, defaultValue);\n\n        public bool IsGameFollowed(string gameName)\n            => SettingsIni.GetBooleanValue(\"Channels\", gameName, false);\n\n        public bool ToggleFavoriteMap(string mapSHA1, string gameModeName, bool isFavorite)\n        {\n            if (string.IsNullOrEmpty(mapSHA1))\n                return isFavorite;\n\n            string favoriteMapKey = FavoriteMapKey(mapSHA1, gameModeName);\n\n            bool isCurrentlyFavorite = FavoriteMaps.Contains(favoriteMapKey);\n            if (isCurrentlyFavorite)\n                FavoriteMaps.Remove(favoriteMapKey);\n            else\n                FavoriteMaps.Add(favoriteMapKey);\n\n            Instance.SaveSettings();\n\n            WriteFavoriteMaps();\n\n            return !isCurrentlyFavorite;\n        }\n\n        private void LoadFavoriteMaps(IniFile iniFile)\n        {\n            FavoriteMaps = new List<string>();\n            bool legacyMapsLoaded = LoadLegacyFavoriteMaps(iniFile);\n            var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS);\n            foreach (KeyValuePair<string, string> keyValuePair in favoriteMapsSection.Keys)\n                FavoriteMaps.Add(keyValuePair.Value);\n\n            if (legacyMapsLoaded)\n                WriteFavoriteMaps();\n        }\n\n        public void WriteFavoriteMaps()\n        {\n            var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS);\n            favoriteMapsSection.RemoveAllKeys();\n            for (int i = 0; i < FavoriteMaps.Count; i++)\n                favoriteMapsSection.AddKey(i.ToString(), FavoriteMaps[i]);\n\n            SaveSettings();\n        }\n\n        /// <summary>\n        /// Checks if a specified map name and game mode name belongs to the favorite map list.\n        /// Name-based favorites are migrated to SHA1.\n        /// </summary>\n        /// <param name=\"mapSHA1\">The SHA1 hash of the map.</param>\n        /// <param name=\"mapName\">The name of the map.</param>\n        /// <param name=\"gameModeName\">The name of the game mode.</param>\n        public bool IsFavoriteMap(string mapSHA1, string mapName, string gameModeName)\n        {\n            // SHA1-based lookup first\n            if (!string.IsNullOrEmpty(mapSHA1) && FavoriteMaps.Contains(FavoriteMapKey(mapSHA1, gameModeName)))\n                return true;\n\n            // Fallback to name-based\n            string nameKey = FavoriteMapKey(mapName, gameModeName);\n            if (FavoriteMaps.Contains(nameKey))\n            {\n                // Migrate to SHA1\n                if (!string.IsNullOrEmpty(mapSHA1))\n                {\n                    string sha1Key = FavoriteMapKey(mapSHA1, gameModeName);\n                    if (!FavoriteMaps.Contains(sha1Key))\n                    {\n                        FavoriteMaps.Add(sha1Key);\n                        WriteFavoriteMaps();\n                    }\n                    // Note: We don't remove the name-based entry here to allow other maps\n                    // with the same name to also migrate. The name-based entry will be\n                    // cleaned up when all maps with that name have been processed.\n                }\n                return true;\n            }\n\n            return false;\n        }\n\n        private string FavoriteMapKey(string identifier, string gameModeName) => $\"{identifier}:{gameModeName}\";\n\n        public void ReloadSettings() => SettingsIni.Reload();\n\n        public void ApplyDefaults()\n        {\n            ForceLowestDetailLevel.SetDefaultIfNonexistent();\n            DoubleTapInterval.SetDefaultIfNonexistent();\n            ScrollDelay.SetDefaultIfNonexistent();\n        }\n\n        public void SaveSettings()\n        {\n            Logger.Log(\"Writing settings INI.\");\n\n            ApplyDefaults();\n            // CleanUpLegacySettings();\n\n            // RA1 reads MultiplayerScoreVolume instead of ScoreVolume\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.RA)\n                SettingsIni.SetDoubleValue(OPTIONS, \"MultiplayerScoreVolume\", SettingsIni.GetDoubleValue(OPTIONS, \"ScoreVolume\", 0.7));\n\n            SettingsIni.WriteIniFile();\n\n            SettingsSaved?.Invoke(this, EventArgs.Empty);\n        }\n\n        public bool IsGameFiltersApplied()\n            => ShowFriendGamesOnly.Value != DEFAULT_SHOW_FRIENDS_ONLY_GAMES\n               || HideLockedGames.Value != DEFAULT_HIDE_LOCKED_GAMES\n               || HidePasswordedGames.Value != DEFAULT_HIDE_PASSWORDED_GAMES\n               || HideIncompatibleGames.Value != DEFAULT_HIDE_INCOMPATIBLE_GAMES\n               || MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT\n               || HasGameOptionFilters();\n\n        public void ResetGameFilters()\n        {\n            ShowFriendGamesOnly.Value = DEFAULT_SHOW_FRIENDS_ONLY_GAMES;\n            HideLockedGames.Value = DEFAULT_HIDE_LOCKED_GAMES;\n            HideIncompatibleGames.Value = DEFAULT_HIDE_INCOMPATIBLE_GAMES;\n            HidePasswordedGames.Value = DEFAULT_HIDE_PASSWORDED_GAMES;\n            MaxPlayerCount.Value = DEFAULT_MAX_PLAYER_COUNT;\n            ResetGameOptionFilters();\n        }\n\n        /// <summary>\n        /// Checks if any game option filters are set.\n        /// </summary>\n        private bool HasGameOptionFilters()\n        {\n            var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);\n            return section != null && section.Keys.Count > 0;\n        }\n\n        /// <summary>\n        /// Clears all game option filters.\n        /// </summary>\n        private void ResetGameOptionFilters()\n        {\n            var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);\n            section?.RemoveAllKeys();\n        }\n\n        /// <summary>\n        /// Used to remove old sections/keys to avoid confusion when viewing the ini file directly.\n        /// </summary>\n        private void CleanUpLegacySettings()\n            => SettingsIni.GetSection(GAME_FILTERS).RemoveKey(\"SortAlpha\");\n\n        /// <summary>\n        /// Previously, favorite maps were stored under a single key under the [Options] section.\n        /// This attempts to read in that legacy key.\n        /// </summary>\n        /// <param name=\"iniFile\"></param>\n        /// <returns>Whether or not legacy favorites were loaded.</returns>\n        private bool LoadLegacyFavoriteMaps(IniFile iniFile)\n        {\n            var legacyFavoriteMaps = new StringListSetting(iniFile, OPTIONS, FAVORITE_MAPS, new List<string>());\n            if (!legacyFavoriteMaps.Value?.Any() ?? true)\n                return false;\n\n            foreach (string favoriteMapKey in legacyFavoriteMaps.Value)\n                FavoriteMaps.Add(favoriteMapKey);\n\n            // remove the old key\n            iniFile.GetSection(OPTIONS).RemoveKey(FAVORITE_MAPS);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/DataWriter.cs",
    "content": "﻿using System;\nusing System.Buffers.Binary;\nusing System.IO;\nusing System.Text;\n\nnamespace ClientCore.Statistics\n{\n    internal static class DataWriter\n    {\n        public static void WriteInt(this Stream stream, int value)\n        {\n            byte[] buffer = new byte[sizeof(int)];\n            BinaryPrimitives.WriteInt32LittleEndian(buffer, value);\n            stream.Write(buffer, 0, sizeof(int));\n        }\n\n        public static void WriteLong(this Stream stream, long value)\n        {\n            byte[] buffer = new byte[sizeof(long)];\n            BinaryPrimitives.WriteInt64LittleEndian(buffer, value);\n            stream.Write(buffer, 0, sizeof(long));\n        }\n\n        public static void WriteBool(this Stream stream, bool value)\n        {\n            stream.WriteByte(Convert.ToByte(value));\n        }\n\n        public static void WriteString(this Stream stream, string value, int reservedSpace, Encoding encoding = null)\n        {\n            if (encoding == null)\n                encoding = Encoding.Unicode;\n\n            byte[] writeBuffer = encoding.GetBytes(value);\n            if (writeBuffer.Length != reservedSpace)\n            {\n                // If the name's byte presentation is not equal to reservedSpace,\n                // let's resize the array\n                byte[] temp = writeBuffer;\n                writeBuffer = new byte[reservedSpace];\n                for (int j = 0; j < temp.Length && j < writeBuffer.Length; j++)\n                    writeBuffer[j] = temp[j];\n            }\n\n            stream.Write(writeBuffer, 0, writeBuffer.Length);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing Rampastring.Tools;\n\nnamespace ClientCore.Statistics.GameParsers\n{\n    public class LogFileStatisticsParser : GenericMatchParser\n    {\n        public LogFileStatisticsParser(MatchStatistics ms, bool isLoadedGame) : base(ms)\n        {\n            this.isLoadedGame = isLoadedGame;\n        }\n\n        private string fileName = \"DTA.log\";\n        private string economyString = \"Economy\"; // RA2/YR do not have economy stat, but a number of built objects.\n        private bool isLoadedGame;\n\n        public void ParseStats(string gamepath, string fileName)\n        {\n            this.fileName = fileName;\n            if (ClientConfiguration.Instance.UseBuiltStatistic) economyString = \"Built\";\n            ParseStatistics(gamepath);\n        }\n\n        protected override void ParseStatistics(string gamepath)\n        {\n            FileInfo statisticsFileInfo = SafePath.GetFile(gamepath, fileName);\n\n            if (!statisticsFileInfo.Exists)\n            {\n                Logger.Log(\"DTAStatisticsParser: Failed to read statistics: the log file does not exist.\");\n                return;\n            }\n\n            Logger.Log(\"Attempting to read statistics from \" + fileName);\n\n            try\n            {\n                using StreamReader reader = new StreamReader(statisticsFileInfo.OpenRead());\n\n                string line;\n\n                List<PlayerStatistics> takeoverAIs = new List<PlayerStatistics>();\n                PlayerStatistics currentPlayer = null;\n\n                bool sawCompletion = false;\n                int numPlayersFound = 0;\n\n                while ((line = reader.ReadLine()) != null)\n                {\n                    if (line.Contains(\": Loser\"))\n                    {\n                        // Player found, game saw completion\n                        sawCompletion = true;\n                        string playerName = line.Substring(0, line.Length - 7);\n                        currentPlayer = Statistics.GetEmptyPlayerByName(playerName);\n\n                        if (isLoadedGame && currentPlayer == null)\n                            currentPlayer = Statistics.Players.Find(p => p.Name == playerName);\n\n                        Logger.Log(\"Found player \" + playerName);\n                        numPlayersFound++;\n\n                        if (currentPlayer == null && playerName == \"Computer\" && numPlayersFound <= Statistics.NumberOfHumanPlayers)\n                        {\n                            // The player has been taken over by an AI during the match\n                            Logger.Log(\"Losing take-over AI found\");\n                            takeoverAIs.Add(new PlayerStatistics(\"Computer\", false, true, false, 0, 10, 255, 1));\n                            currentPlayer = takeoverAIs[takeoverAIs.Count - 1];\n                        }\n\n                        if (currentPlayer != null)\n                            currentPlayer.SawEnd = true;\n                    }\n                    else if (line.Contains(\": Winner\"))\n                    {\n                        // Player found, game saw completion\n                        sawCompletion = true;\n                        string playerName = line.Substring(0, line.Length - 8);\n                        currentPlayer = Statistics.GetEmptyPlayerByName(playerName);\n\n                        if (isLoadedGame && currentPlayer == null)\n                            currentPlayer = Statistics.Players.Find(p => p.Name == playerName);\n\n                        Logger.Log(\"Found player \" + playerName);\n                        numPlayersFound++;\n\n                        if (currentPlayer == null && playerName == \"Computer\" && numPlayersFound <= Statistics.NumberOfHumanPlayers)\n                        {\n                            // The player has been taken over by an AI during the match\n                            Logger.Log(\"Winning take-over AI found\");\n                            takeoverAIs.Add(new PlayerStatistics(\"Computer\", false, true, false, 0, 10, 255, 1));\n                            currentPlayer = takeoverAIs[takeoverAIs.Count - 1];\n                        }\n\n                        if (currentPlayer != null)\n                        {\n                            currentPlayer.SawEnd = true;\n                            currentPlayer.Won = true;\n                        }\n                    }\n                    else if (line.Contains(\"Game loop finished. Average FPS\"))\n                    {\n                        // Game loop finished. Average FPS = <integer>\n                        string fpsString = line.Substring(34);\n                        Statistics.AverageFPS = Int32.Parse(fpsString);\n                    }\n\n                    if (currentPlayer == null || line.Length < 1)\n                        continue;\n\n                    line = line.Substring(1);\n\n                    if (line.StartsWith(\"Lost = \"))\n                        currentPlayer.Losses = Int32.Parse(line.Substring(7));\n                    else if (line.StartsWith(\"Kills = \"))\n                        currentPlayer.Kills = Int32.Parse(line.Substring(8));\n                    else if (line.StartsWith(\"Score = \"))\n                        currentPlayer.Score = Int32.Parse(line.Substring(8));\n                    else if (line.StartsWith(economyString + \" = \"))\n                        currentPlayer.Economy = Int32.Parse(line.Substring(economyString.Length + 2));\n                }\n\n                // Check empty players for take-over by AIs\n                if (takeoverAIs.Count == 1)\n                {\n                    PlayerStatistics ai = takeoverAIs[0];\n\n                    PlayerStatistics ps = Statistics.GetFirstEmptyPlayer();\n\n                    ps.Losses = ai.Losses;\n                    ps.Kills = ai.Kills;\n                    ps.Score = ai.Score;\n                    ps.Economy = ai.Economy;\n                }\n                else if (takeoverAIs.Count > 1)\n                {\n                    // If there's multiple take-over AI players, we have no way of figuring out\n                    // which AI represents which player, so let's just add the AIs into the player list\n                    // (then the user viewing the statistics can figure it out themselves)\n                    for (int i = 0; i < takeoverAIs.Count; i++)\n                    {\n                        takeoverAIs[i].SawEnd = false;\n                        Statistics.AddPlayer(takeoverAIs[i]);\n                    }\n                }\n\n                Statistics.SawCompletion = sawCompletion;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"DTAStatisticsParser: Error parsing statistics from match! Message: \" + ex.ToString());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/GenericMatchParser.cs",
    "content": "﻿namespace ClientCore.Statistics\n{\n    public abstract class GenericMatchParser\n    {\n        public MatchStatistics Statistics {get; set;}\n\n        public GenericMatchParser(MatchStatistics ms)\n        {\n            Statistics = ms;\n        }\n\n        protected abstract void ParseStatistics(string gamepath);\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/GenericStatisticsManager.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace ClientCore.Statistics\n{\n    public abstract class GenericStatisticsManager\n    {\n        protected List<MatchStatistics> Statistics = new List<MatchStatistics>();\n\n        protected static string GetStatDatabaseVersion(string scorePath)\n        {\n            if (!File.Exists(scorePath))\n            {\n                return null;\n            }\n\n            using (StreamReader reader = new StreamReader(scorePath))\n            {\n                char[] versionBuffer = new char[4];\n                reader.Read(versionBuffer, 0, versionBuffer.Length);\n\n                String s = new String(versionBuffer);\n                return s;\n            }\n        }\n\n        public abstract void ReadStatistics(string gamePath);\n\n        public int GetMatchCount() { return Statistics.Count; }\n\n        public MatchStatistics GetMatchByIndex(int index)\n        {\n            return Statistics[index];\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/MatchStatistics.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing ClientCore.Statistics.GameParsers;\nusing Rampastring.Tools;\n\nnamespace ClientCore.Statistics\n{\n    public class MatchStatistics\n    {\n        public MatchStatistics() { }\n\n        public MatchStatistics(string gameVersion, int gameId, string mapName, string gameMode, int numHumans, bool mapIsCoop = false)\n        {\n            GameVersion = gameVersion;\n            GameID = gameId;\n            DateAndTime = DateTime.Now;\n            MapName = mapName;\n            GameMode = gameMode;\n            NumberOfHumanPlayers = numHumans;\n            MapIsCoop = mapIsCoop;\n        }\n\n        public List<PlayerStatistics> Players = new List<PlayerStatistics>();\n\n        public int LengthInSeconds { get; set; }\n\n        public DateTime DateAndTime { get; set; }\n\n        public string GameVersion { get; set; }\n\n        public string MapName { get; set; }\n\n        public string GameMode { get; set; }\n\n        public bool SawCompletion { get; set; }\n\n        public int NumberOfHumanPlayers { get; set; }\n\n        public int AverageFPS { get; set; }\n\n        public int GameID { get; set; }\n\n        public bool MapIsCoop { get; set; }\n\n        public bool IsValidForStar { get; set; } = true;\n\n        public void AddPlayer(string name, bool isLocal, bool isAI, bool isSpectator,\n            int side, int team, int color, int aiLevel)\n        {\n            PlayerStatistics ps = new PlayerStatistics(name, isLocal, isAI, isSpectator, \n                side, team, color, aiLevel);\n            Players.Add(ps);\n        }\n\n        public void AddPlayer(PlayerStatistics ps)\n        {\n            Players.Add(ps);\n        }\n\n        public void ParseStatistics(string gamePath, string gameName, bool isLoadedGame)\n        {\n            Logger.Log(\"Parsing game statistics.\");\n\n            LengthInSeconds = (int)(DateTime.Now - DateAndTime).TotalSeconds;\n\n            var parser = new LogFileStatisticsParser(this, isLoadedGame);\n            parser.ParseStats(gamePath, ClientConfiguration.Instance.StatisticsLogFileName);\n        }\n\n        public PlayerStatistics GetEmptyPlayerByName(string playerName)\n        {\n            foreach (PlayerStatistics ps in Players)\n            {\n                if (ps.Name == playerName && ps.Losses == 0 && ps.Score == 0)\n                    return ps;\n            }\n\n            return null;\n        }\n\n        public PlayerStatistics GetFirstEmptyPlayer()\n        {\n            foreach (PlayerStatistics ps in Players)\n            {\n                if (ps.Losses == 0 && ps.Score == 0)\n                    return ps;\n            }\n\n            return null;\n        }\n\n        public int GetPlayerCount()\n        {\n            return Players.Count;\n        }\n\n        public PlayerStatistics GetPlayer(int index)\n        {\n            return Players[index];\n        }\n\n        public void Write(Stream stream)\n        {\n            // Game length\n            stream.WriteInt(LengthInSeconds);\n\n            // Game version, 8 bytes, ASCII\n            stream.WriteString(GameVersion, 8, Encoding.ASCII);\n\n            // Date and time, 8 bytes\n            stream.WriteLong(DateAndTime.ToBinary());\n            // SawCompletion, 1 byte\n            stream.WriteBool(SawCompletion);\n            // Number of players, 1 byte\n            stream.WriteByte(Convert.ToByte(GetPlayerCount()));\n            // Average FPS, 4 bytes\n            stream.WriteInt(AverageFPS);\n            // Map name, 128 bytes (64 chars), Unicode\n            stream.WriteString(MapName, 128);\n            // Game mode, 64 bytes (32 chars), Unicode\n            stream.WriteString(GameMode, 64);\n            // Unique game ID, 4 bytes\n            stream.WriteInt(GameID);\n            // Whether game options were valid for earning a star, 1 byte\n            stream.WriteBool(IsValidForStar);\n\n            // Write player info\n            for (int i = 0; i < GetPlayerCount(); i++)\n            {\n                PlayerStatistics ps = GetPlayer(i);\n                ps.Write(stream);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/PlayerStatistics.cs",
    "content": "﻿using System;\nusing System.IO;\n\nnamespace ClientCore.Statistics\n{\n    public class PlayerStatistics\n    {\n        public PlayerStatistics() { }\n\n        public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator,\n            int side, int team, int color, int aiLevel)\n        {\n            Name = name;\n            IsLocalPlayer = isLocal;\n            IsAI = isAi;\n            WasSpectator = isSpectator;\n            Side = side;\n            Team = team;\n            Color = color;\n            AILevel = aiLevel;\n        }\n\n        public string Name { get; set; }\n        public int Kills { get; set; }\n        public int Losses { get; set; }\n        public int Economy { get; set; }\n        public int Score { get; set; }\n        public int Side { get; set; }\n        public int Team { get; set; }\n        public int AILevel { get; set; }\n        public bool SawEnd { get; set; }\n        public bool WasSpectator { get; set; }\n        public bool Won { get; set; }\n        public bool IsLocalPlayer { get; set; }\n        public bool IsAI { get; set; }\n        public int Color { get; set; } = 255;\n\n        public void Write(Stream stream)\n        {\n            stream.WriteInt(Economy);\n            // 1 byte for IsAI\n            stream.WriteBool(IsAI);\n            // 1 byte for IsLocalPlayer\n            stream.WriteBool(IsLocalPlayer);\n            // 4 bytes for kills\n            stream.WriteInt(Kills);\n            // 4 bytes for losses\n            stream.WriteInt(Losses);\n            // Name takes 32 bytes\n            stream.WriteString(Name, 32);\n            // 1 byte for SawEnd\n            stream.WriteBool(SawEnd);\n            // 4 bytes for Score\n            stream.WriteInt(Score);\n            // 1 byte for Side\n            stream.WriteByte(Convert.ToByte(Side));\n            // 1 byte for Team\n            stream.WriteByte(Convert.ToByte(Team));\n            // 1 byte color Color\n            stream.WriteByte(Convert.ToByte(Color));\n            // 1 byte for WasSpectator\n            stream.WriteBool(WasSpectator);\n            // 1 byte for Won\n            stream.WriteBool(Won);\n            // 1 byte for AI level\n            stream.WriteByte(Convert.ToByte(AILevel));\n        }\n    }\n}\n"
  },
  {
    "path": "ClientCore/Statistics/StatisticsManager.cs",
    "content": "﻿using System;\nusing System.Buffers.Binary;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.IO;\nusing System.Linq;\nusing Rampastring.Tools;\nusing System.Diagnostics;\n\nnamespace ClientCore.Statistics\n{\n    public class StatisticsManager : GenericStatisticsManager\n    {\n        private const string VERSION = \"1.06\";\n        private const string SCORE_FILE_PATH = \"Client/dscore.dat\";\n        private const string OLD_SCORE_FILE_PATH = \"dscore.dat\";\n        private static StatisticsManager _instance;\n\n        private bool _statisticsInitialized = false;\n\n        public event EventHandler GameAdded;\n\n\n        public static StatisticsManager Instance\n        {\n            get\n            {\n                if (_instance == null)\n                    _instance = new StatisticsManager();\n                return _instance;\n            }\n        }\n\n        public override void ReadStatistics(string gamePath)\n        {\n            FileInfo scoreFileInfo = SafePath.GetFile(gamePath, SCORE_FILE_PATH);\n\n            if (!scoreFileInfo.Exists)\n            {\n                Logger.Log(\"Skipping reading statistics because the file doesn't exist!\");\n                _statisticsInitialized = true;\n                return;\n            }\n\n            Logger.Log(\"Reading statistics.\");\n\n            Statistics.Clear();\n\n            FileInfo oldScoreFileInfo = SafePath.GetFile(gamePath, OLD_SCORE_FILE_PATH);\n            bool resave = ReadFile(oldScoreFileInfo.FullName);\n            bool resaveNew = ReadFile(scoreFileInfo.FullName);\n\n            PurgeStats();\n\n            if (resave || resaveNew)\n            {\n                if (oldScoreFileInfo.Exists)\n                {\n                    File.Copy(oldScoreFileInfo.FullName, SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, \"dscore_old.dat\"));\n                    SafePath.DeleteFileIfExists(oldScoreFileInfo.FullName);\n                }\n\n                SaveDatabase();\n            }\n\n            _statisticsInitialized = true;\n        }\n\n        /// <summary>\n        /// Reads a statistics file.\n        /// </summary>\n        /// <param name=\"filePath\">The path to the statistics file.</param>\n        /// <returns>A bool that determines whether the database should be re-saved.</returns>\n        private bool ReadFile(string filePath)\n        {\n            bool returnValue = false;\n\n            try\n            {\n                string databaseVersion = GetStatDatabaseVersion(filePath);\n\n                if (databaseVersion == null)\n                    return false; // No score database exists\n\n                switch (databaseVersion)\n                {\n                    case \"1.00\":\n                    case \"1.01\":\n                        ReadDatabase(filePath, 0);\n                        returnValue = true;\n                        break;\n                    case \"1.02\":\n                        ReadDatabase(filePath, 2);\n                        returnValue = true;\n                        break;\n                    case \"1.03\":\n                        ReadDatabase(filePath, 3);\n                        returnValue = true;\n                        break;\n                    case \"1.04\":\n                        ReadDatabase(filePath, 4);\n                        returnValue = true;\n                        break;\n                    case \"1.05\":\n                        ReadDatabase(filePath, 5);\n                        returnValue = true;\n                        break;\n                    case \"1.06\":\n                        ReadDatabase(filePath, 6);\n                        break;\n                    default:\n                        throw new InvalidDataException(\"Invalid version for \" + filePath + \": \" + databaseVersion);\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Error reading statistics: \" + ex.ToString());\n            }\n\n            return returnValue;\n        }\n\n        private void ReadDatabase(string filePath, int version)\n        {\n            // TODO split this function with the MatchStatistics and PlayerStatistics classes\n\n            try\n            {\n                using (FileStream fs = File.OpenRead(filePath))\n                {\n                    fs.Position = 4; // Skip version\n                    byte[] readBuffer = new byte[128];\n                    fs.Read(readBuffer, 0, 4); // First 4 bytes following the version mean the amount of games\n                    int gameCount = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n\n                    for (int i = 0; i < gameCount; i++)\n                    {\n                        MatchStatistics ms = new MatchStatistics();\n\n                        // First 4 bytes of game info is the length in seconds\n                        fs.Read(readBuffer, 0, 4);\n                        int lengthInSeconds = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                        ms.LengthInSeconds = lengthInSeconds;\n                        // Next 8 are the game version\n                        fs.Read(readBuffer, 0, 8);\n                        ms.GameVersion = System.Text.Encoding.ASCII.GetString(readBuffer, 0, 8);\n                        // Then comes the date and time, also 8 bytes\n                        fs.Read(readBuffer, 0, 8);\n                        long dateData = BinaryPrimitives.ReadInt64LittleEndian(readBuffer);\n                        ms.DateAndTime = DateTime.FromBinary(dateData);\n                        // Then one byte for SawCompletion\n                        fs.Read(readBuffer, 0, 1);\n                        ms.SawCompletion = Convert.ToBoolean(readBuffer[0]);\n                        // Then 1 byte for the amount of players\n                        fs.Read(readBuffer, 0, 1);\n                        int playerCount = readBuffer[0];\n                        if (version > 0)\n                        {\n                            // 4 bytes for average FPS\n                            fs.Read(readBuffer, 0, 4);\n                            ms.AverageFPS = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                        }\n\n                        int mapNameLength = 64;\n\n                        if (version > 3)\n                        {\n                            mapNameLength = 128;\n                        }\n\n                        // Map name, 64 or 128 bytes of Unicode depending on version\n                        fs.Read(readBuffer, 0, mapNameLength);\n                        ms.MapName = Encoding.Unicode.GetString(readBuffer).Replace(\"\\0\", \"\");\n\n                        // Game mode, 64 bytes\n                        fs.Read(readBuffer, 0, 64);\n                        ms.GameMode = Encoding.Unicode.GetString(readBuffer, 0, 64).Replace(\"\\0\", \"\");\n\n                        if (version > 2)\n                        {\n                            // Unique game ID, 32 bytes (int32)\n                            fs.Read(readBuffer, 0, 4);\n                            ms.GameID = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                        }\n\n                        if (version > 5)\n                        {\n                            fs.Read(readBuffer, 0, 1);\n                            ms.IsValidForStar = Convert.ToBoolean(readBuffer[0]);\n                        }\n\n                        // Player info comes right after the general match info\n                        for (int j = 0; j < playerCount; j++)\n                        {\n                            PlayerStatistics ps = new PlayerStatistics();\n\n                            if (version > 4)\n                            {\n                                // Economy is shared for the Built stat in YR\n                                fs.Read(readBuffer, 0, 4);\n                                ps.Economy = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                            }\n                            else\n                            {\n                                // Economy is between 0 and 100 in old versions, so it takes only one byte\n                                fs.Read(readBuffer, 0, 1);\n                                ps.Economy = readBuffer[0];\n                            }\n\n                            // IsAI is a bool, so obviously one byte\n                            fs.Read(readBuffer, 0, 1);\n                            ps.IsAI = Convert.ToBoolean(readBuffer[0]);\n                            // IsLocalPlayer is also a bool\n                            fs.Read(readBuffer, 0, 1);\n                            ps.IsLocalPlayer = Convert.ToBoolean(readBuffer[0]);\n                            // Kills take 4 bytes\n                            fs.Read(readBuffer, 0, 4);\n                            ps.Kills = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                            // Losses also take 4 bytes\n                            fs.Read(readBuffer, 0, 4);\n                            ps.Losses = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                            // 32 bytes for the name\n                            fs.Read(readBuffer, 0, 32);\n                            ps.Name = System.Text.Encoding.Unicode.GetString(readBuffer, 0, 32);\n                            ps.Name = ps.Name.Replace(\"\\0\", String.Empty);\n                            // 1 byte for SawEnd\n                            fs.Read(readBuffer, 0, 1);\n                            ps.SawEnd = Convert.ToBoolean(readBuffer[0]);\n                            // 4 bytes for Score\n                            fs.Read(readBuffer, 0, 4);\n                            ps.Score = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);\n                            // 1 byte for Side\n                            fs.Read(readBuffer, 0, 1);\n                            ps.Side = readBuffer[0];\n                            // 1 byte for Team\n                            fs.Read(readBuffer, 0, 1);\n                            ps.Team = readBuffer[0];\n                            if (version > 2)\n                            {\n                                // 1 byte for Color\n                                fs.Read(readBuffer, 0, 1);\n                                ps.Color = readBuffer[0];\n                            }\n                            // 1 byte for WasSpectator\n                            fs.Read(readBuffer, 0, 1);\n                            ps.WasSpectator = Convert.ToBoolean(readBuffer[0]);\n                            // 1 byte for Won\n                            fs.Read(readBuffer, 0, 1);\n                            ps.Won = Convert.ToBoolean(readBuffer[0]);\n                            // 1 byte for AI level\n                            fs.Read(readBuffer, 0, 1);\n                            ps.AILevel = readBuffer[0];\n\n                            ms.AddPlayer(ps);\n\n                            if (!ps.IsAI)\n                                ms.NumberOfHumanPlayers++;\n                        }\n\n                        if (ms.Players.Find(p => p.IsLocalPlayer && !p.IsAI) == null)\n                            continue;\n\n                        Statistics.Add(ms);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Reading the statistics file failed! Message: \" + ex.ToString());\n            }\n        }\n\n        public void PurgeStats()\n        {\n            int removedCount = 0;\n\n            for (int i = 0; i < Statistics.Count; i++)\n            {\n                if (Statistics[i].LengthInSeconds < 60)\n                {\n                    Logger.Log(\"Removing match on \" + Statistics[i].MapName + \" because it's too short.\");\n                    Statistics.RemoveAt(i);\n                    i--;\n                    removedCount++;\n                }\n            }\n\n            if (removedCount > 0)\n                SaveDatabase();\n        }\n\n        public void ClearDatabase()\n        {\n            Statistics.Clear();\n            CreateDummyFile();\n            _statisticsInitialized = true;\n        }\n\n        public void AddMatchAndSaveDatabase(bool addMatch, MatchStatistics ms)\n        {\n            if (ms == null)\n            {\n                Logger.Log(\"Skipping adding match to statistics because match statistics is null.\");\n                return;\n            }\n\n            // 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.\n            if (ms.GetPlayerCount() <= 1 && !ms.MapIsCoop)\n            {\n                Logger.Log(\"Skipping adding match to statistics because game only had one player.\");\n                return;\n            }\n\n            if (ms.LengthInSeconds < 60)\n            {\n                Logger.Log(\"Skipping adding match to statistics because the game was cancelled.\");\n                return;\n            }\n\n            if (addMatch)\n            {\n                Statistics.Add(ms);\n                GameAdded?.Invoke(this, EventArgs.Empty);\n            }\n\n            FileInfo scoreFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH);\n\n            if (!scoreFileInfo.Exists)\n            {\n                CreateDummyFile();\n            }\n\n            Logger.Log(\"Writing game info to statistics file.\");\n\n            using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite))\n            {\n                fs.Position = 4; // First 4 bytes after the version mean the amount of games\n                fs.WriteInt(Statistics.Count);\n\n                fs.Position = fs.Length;\n                ms.Write(fs);\n            }\n\n            Logger.Log(\"Finished writing statistics.\");\n        }\n\n        private void CreateDummyFile()\n        {\n            Logger.Log(\"Creating empty statistics file.\");\n\n            using StreamWriter sw = new StreamWriter(SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH).Create());\n            sw.Write(VERSION);\n        }\n\n        /// <summary>\n        /// Deletes the statistics file on the file system and rewrites it.\n        /// </summary>\n        public void SaveDatabase()\n        {\n            FileInfo scoreFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH);\n            SafePath.DeleteFileIfExists(scoreFileInfo.FullName);\n            CreateDummyFile();\n\n            using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite))\n            {\n                fs.Position = 4; // First 4 bytes after the version mean the amount of games\n                fs.WriteInt(Statistics.Count);\n\n                foreach (MatchStatistics ms in Statistics)\n                {\n                    ms.Write(fs);\n                }\n            }\n        }\n\n        public bool HasBeatCoOpMap(string mapName, string gameMode)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            List<MatchStatistics> matches = new List<MatchStatistics>();\n\n            // Filter out unfitting games\n            foreach (MatchStatistics ms in Statistics)\n            {\n                if (ms.SawCompletion &&\n                    ms.MapName == mapName &&\n                    ms.GameMode == gameMode)\n                {\n                    if (ms.Players[0].Won)\n                        return true;\n                }\n            }\n\n            return false;\n        }\n\n        public int GetCoopRankForDefaultMap(string mapName, int requiredPlayerCount)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            List<MatchStatistics> matches = new List<MatchStatistics>();\n\n            // Filter out unfitting games\n            foreach (MatchStatistics ms in Statistics)\n            {\n                if (!ms.SawCompletion)\n                    continue;\n\n                if (!ms.IsValidForStar)\n                    continue;\n\n                if (ms.MapName != mapName)\n                    continue;\n\n                if (ms.Players.Count != requiredPlayerCount)\n                    continue;\n\n                if (ms.Players.Count(ps => !ps.IsAI && !ps.WasSpectator) > 1 &&\n                    ms.Players.Find(ps => ps.IsAI) != null)\n                    matches.Add(ms);\n            }\n\n            int rank = -1;\n\n            foreach (MatchStatistics ms in matches)\n            {\n                rank = Math.Max(rank, GetRankForCoopMatch(ms));\n            }\n\n            return rank;\n        }\n\n        int GetRankForCoopMatch(MatchStatistics ms)\n        {\n            PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);\n\n            if (localPlayer == null || !localPlayer.Won)\n                return -1;\n\n            if (ms.Players.Find(p => p.WasSpectator) != null)\n                return -1; // Don't allow matches with spectators\n\n            if (ms.Players.Count(p => !p.IsAI && p.Team != localPlayer.Team) > 0)\n                return -1; // Don't allow matches with human players who were on a different team\n\n            if (ms.Players.Find(p => p.Team == 0) != null)\n                return -1; // Matches with non-allied players are discarded\n\n            if (ms.Players.All(ps => ps.Team == localPlayer.Team))\n                return -1; // Discard matches that had no enemies\n\n            int[] teamMemberCounts = new int[5];\n            int lowestEnemyAILevel = 2;\n            int highestAllyAILevel = 0;\n\n            for (int i = 0; i < ms.Players.Count; i++)\n            {\n                PlayerStatistics ps = ms.GetPlayer(i);\n\n                teamMemberCounts[ps.Team]++;\n\n                if (!ps.IsAI)\n                {\n                    continue;\n                }\n\n                if (ps.Team > 0 && ps.Team == localPlayer.Team)\n                {\n                    if (ps.AILevel > highestAllyAILevel)\n                        highestAllyAILevel = ps.AILevel;\n                }\n                else\n                {\n                    if (ps.AILevel < lowestEnemyAILevel)\n                        lowestEnemyAILevel = ps.AILevel;\n                }\n            }\n\n            if (lowestEnemyAILevel < highestAllyAILevel)\n            {\n                // Check that the player's AI allies weren't stronger \n                return -1;\n            }\n\n            // Check that all teams had at least as many players\n            // as the local player's team\n            int allyCount = teamMemberCounts[localPlayer.Team];\n\n            for (int i = 1; i < 5; i++)\n            {\n                if (i == localPlayer.Team)\n                    continue;\n\n                if (teamMemberCounts[i] > 0)\n                {\n                    if (teamMemberCounts[i] < allyCount)\n                        return -1;\n                }\n            }\n\n            return lowestEnemyAILevel;\n        }\n\n        public bool HasWonMapInPvP(string mapName, string gameMode, int requiredPlayerCount)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            List<MatchStatistics> matches = new List<MatchStatistics>();\n\n            foreach (MatchStatistics ms in Statistics)\n            {\n                if (!ms.SawCompletion)\n                    continue;\n\n                if (!ms.IsValidForStar)\n                    continue;\n\n                if (ms.MapName != mapName)\n                    continue;\n\n                if (ms.GameMode != gameMode)\n                    continue;\n\n                if (ms.Players.Count(ps => !ps.WasSpectator) != requiredPlayerCount)\n                    continue;\n\n                if (ms.Players.Find(ps => ps.IsAI) != null)\n                    continue;\n\n                PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);\n\n                if (localPlayer == null)\n                    continue;\n\n                if (localPlayer.WasSpectator)\n                    continue;\n\n                if (!localPlayer.Won)\n                    continue;\n\n                int[] teamMemberCounts = new int[5];\n\n                ms.Players.FindAll(ps => !ps.WasSpectator).ForEach(ps => teamMemberCounts[ps.Team]++);\n\n                if (localPlayer.Team > 0)\n                {\n                    int lowestEnemyTeamMemberCount = int.MaxValue;\n\n                    for (int i = 1; i < 5; i++)\n                    {\n                        if (i != localPlayer.Team && teamMemberCounts[i] > 0)\n                        {\n                            if (teamMemberCounts[i] < lowestEnemyTeamMemberCount)\n                                lowestEnemyTeamMemberCount = teamMemberCounts[i];\n                        }\n                    }\n\n                    if (lowestEnemyTeamMemberCount > teamMemberCounts[localPlayer.Team])\n                        continue;\n\n                    return true;\n                }\n\n                if (ms.Players.Count(ps => !ps.WasSpectator) > 1)\n                    return true;\n            }\n\n            return false;\n        }\n\n        public int GetSkirmishRankForDefaultMap(string mapName, int requiredPlayerCount)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            List<MatchStatistics> matches = new List<MatchStatistics>();\n\n            // Filter out unfitting games\n            foreach (MatchStatistics ms in Statistics)\n            {\n                if (ms.SawCompletion &&\n                    ms.IsValidForStar &&\n                    ms.MapName == mapName &&\n                    ms.Players.Count == requiredPlayerCount &&\n                    ms.Players.Count(p => !p.IsAI) == 1)\n                    matches.Add(ms);\n            }\n\n            int rank = -1;\n\n            foreach (MatchStatistics ms in matches)\n            {\n                // TODO This code turned out pretty ugly, should design it better\n\n                PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);\n\n                if (localPlayer == null || !localPlayer.Won)\n                    continue;\n\n                int[] teamMemberCounts = new int[5];\n                int lowestEnemyAILevel = 2;\n                int highestAllyAILevel = 0;\n\n                for (int i = 0; i < ms.Players.Count; i++)\n                {\n                    PlayerStatistics ps = ms.GetPlayer(i);\n\n                    teamMemberCounts[ps.Team]++;\n\n                    if (ps.IsLocalPlayer)\n                    {\n                        continue;\n                    }\n\n                    if (ps.Team > 0 && ps.Team == localPlayer.Team)\n                    {\n                        if (ps.AILevel > highestAllyAILevel)\n                            highestAllyAILevel = ps.AILevel;\n                    }\n                    else\n                    {\n                        if (ps.AILevel < lowestEnemyAILevel)\n                            lowestEnemyAILevel = ps.AILevel;\n                    }\n                }\n\n                if (lowestEnemyAILevel < highestAllyAILevel)\n                {\n                    // Check that the player's AI allies weren't stronger \n                    continue;\n                }\n\n                if (localPlayer.Team > 0)\n                {\n                    // Check that all teams had at least as many players as the human player's team\n\n                    int allyCount = teamMemberCounts[localPlayer.Team];\n                    bool pass = true;\n\n                    for (int i = 1; i < 5; i++)\n                    {\n                        if (i == localPlayer.Team)\n                            continue;\n\n                        if (teamMemberCounts[i] > 0)\n                        {\n                            if (teamMemberCounts[i] < allyCount)\n                            {\n                                // The enemy team has fewer players than the player's team\n                                pass = false;\n                                break;\n                            }\n                        }\n                    }\n\n                    if (!pass)\n                        continue;\n\n                    // Check that there is a team other than the players' team that is at least as large\n                    pass = false;\n                    for (int i = 1; i < 5; i++)\n                    {\n                        if (i == localPlayer.Team)\n                            continue;\n\n                        if (teamMemberCounts[i] >= allyCount)\n                        {\n                            pass = true;\n                            break;\n                        }\n                    }\n\n                    if (!pass)\n                        continue;\n                }\n\n                if (rank < lowestEnemyAILevel)\n                {\n                    rank = lowestEnemyAILevel;\n\n                    if (rank == 2)\n                        return rank; // Best possible rank\n                }\n            }\n\n            return rank;\n        }\n\n        public bool IsGameIdUnique(int gameId)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            return Statistics.Find(m => m.GameID == gameId) == null;\n        }\n\n        public MatchStatistics GetMatchWithGameID(int gameId)\n        {\n            Debug.Assert(_statisticsInitialized, \"StatisticsManager must have been initialized before.\");\n            return Statistics.Find(m => m.GameID == gameId);\n        }\n\n    }\n}\n"
  },
  {
    "path": "ClientGUI/ClientGUI.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <Description>CnCNet Client UI Library</Description>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ClientCore\\ClientCore.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Rampastring.XNAUI\\Rampastring.XNAUI.csproj\" />\n  </ItemGroup>\n  <ItemGroup Condition=\"$(Configuration.Contains('GL'))\">\n    <!--Remove WinForm-->\n    <Compile Remove=\"IME\\WinFormsIMEHandler.cs\" />\n    <None Include=\"IME\\WinFormsIMEHandler.cs\" />\n  </ItemGroup>\n  <ItemGroup Condition=\"!$(Configuration.Contains('GL'))\">\n    <!--Remove SDL-->\n    <Compile Remove=\"IME\\SdlIMEHandler.cs\" />\n    <None Include=\"IME\\SdlIMEHandler.cs\" />\n    <PackageReference Include=\"ImeSharp\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "ClientGUI/ClientGUICreator.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing Microsoft.Extensions.DependencyInjection;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// This gui creator helps in the registration of XNAControl based controls that can be used via dependency injection\n    /// or through the INI system.\n    /// </summary>\n    public static class ClientGUICreator\n    {\n        private static List<Type> controlTypes = new();\n\n        private static IServiceProvider serviceProvider;\n\n        /// <summary>\n        /// Adds a control type as a singleton to our list of known control types.\n        ///\n        /// When a control is added as singleton, the same instance will be returned every time one is requested by the control's name.\n        /// </summary>\n        /// <param name=\"serviceCollection\">Service collection for our dependency injection.</param>\n        /// <param name=\"controlType\">The control type to add.</param>\n        /// <returns>IServiceCollection.</returns>\n        public static IServiceCollection AddSingletonXnaControl<T>(this IServiceCollection serviceCollection)\n        {\n            Type controlType = typeof(T);\n            AddXnaControl(controlType);\n            return serviceCollection.AddSingleton(controlType, provider => GetXnaControl(provider, controlType.Name));\n        }\n\n        /// <summary>\n        /// Adds a control type as a transient to our list of known control types.\n        ///\n        /// When a control is added as transient, a new instance will be instantiated every time one is requested by the control's name.\n        /// </summary>\n        /// <param name=\"serviceCollection\">Service collection for our dependency injection.</param>\n        /// <param name=\"controlType\">The control type to add.</param>\n        /// <returns>IServiceCollection.</returns>\n        public static IServiceCollection AddTransientXnaControl<T>(this IServiceCollection serviceCollection)\n        {\n            Type controlType = typeof(T);\n            AddXnaControl(controlType);\n            return serviceCollection.AddTransient(controlType, provider => GetXnaControl(provider, controlType.Name));\n        }\n\n        /// <summary>\n        /// This is typically called during control initialization via the INI UI system.\n        /// </summary>\n        /// <param name=\"controlTypeName\">The name of the control to instantiate.</param>\n        /// <returns>XNAControl instance.</returns>\n        public static XNAControl GetXnaControl(string controlTypeName) => GetXnaControl(serviceProvider, controlTypeName);\n\n        /// <summary>\n        /// Adds the control type to our list of known controls for instantiation.\n        /// </summary>\n        /// <param name=\"controlType\">The control type to add.</param>\n        /// <exception cref=\"Exception\">\n        /// If this control is not a sub-class of XNAControl or is not an XNAControl itself.\n        /// OR, this component type is added more than once.\n        /// </exception>\n        private static void AddXnaControl(Type controlType)\n        {\n            if (!controlType.IsSubclassOf(typeof(XNAControl)) && controlType != typeof(XNAControl))\n                throw new Exception($\"{controlType.Name} is not a sub class of {nameof(XNAControl)}\");\n\n            ValidateNonDuplicateControlType(controlType);\n\n            controlTypes.Add(controlType);\n        }\n\n        /// <summary>\n        /// Because the INI system retrieves controls by its <see cref=\"Type.Name\"/>, we need to make sure that\n        /// duplicates are not being registered with the same base name as another control.\n        /// </summary>\n        /// <param name=\"controlType\">The Type to validate.</param>\n        /// <exception cref=\"Exception\">If another control was registered with the same name.</exception>\n        private static void ValidateNonDuplicateControlType(Type controlType)\n        {\n            if (controlTypes.Any(c => c.Name == controlType.Name))\n                throw new Exception($\"A control type with name {controlType.Name} has already been registered.\");\n        }\n\n        /// <summary>\n        /// This is the \"factory\" that is used to instantiate a control.\n        ///\n        /// If this function is called for a singleton, it will only be called ONCE for a given <see cref=\"controlTypeName\"/>\n        /// </summary>\n        /// <param name=\"provider\">Our dependency injection service provider.</param>\n        /// <param name=\"controlTypeName\">The name of the control type to instantiate.</param>\n        /// <returns>XNAControl instance.</returns>\n        /// <exception cref=\"Exception\">If the control type was not registered with our service provider.</exception>\n        private static XNAControl GetXnaControl(IServiceProvider provider, string controlTypeName)\n        {\n            serviceProvider ??= provider;\n            Type controlType = controlTypes.SingleOrDefault(control => control.Name == controlTypeName);\n            if (controlType == null)\n                throw new Exception($\"Control type {controlTypeName} was not registered with ServiceCollection in GameClass\");\n\n            ConstructorInfo constructor = controlType.GetConstructors().First();\n            IEnumerable<object> parameterInstances = constructor.GetParameters().Select(param => GetTypeInstance(param.ParameterType));\n\n            return (XNAControl)constructor.Invoke(parameterInstances.ToArray());\n        }\n\n        /// <summary>\n        /// Attempts to get an instance of a specific type from our serviced provider.\n        /// </summary>\n        /// <param name=\"type\">The type to instantiate.</param>\n        /// <returns>An instance of the type specified.</returns>\n        /// <exception cref=\"Exception\">If the type was not registered with our service provider.</exception>\n        private static object GetTypeInstance(Type type)\n            => serviceProvider.GetService(type) ?? throw new Exception($\"Control type {type.Name} was not registered with ServiceCollection in GameClass\");\n    }\n}"
  },
  {
    "path": "ClientGUI/DarkeningPanel.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing System;\nusing Rampastring.XNAUI;\nusing Microsoft.Xna.Framework;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A panel that darkens the whole screen.\n    /// </summary>\n    public class DarkeningPanel : XNAPanel\n    {\n        public const float ALPHA_RATE = 0.6f;\n        private bool _fadeEnabled = true;\n\n        public DarkeningPanel(WindowManager windowManager) : base(windowManager)\n        {\n            DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET;\n        }\n\n        public event EventHandler Hidden;\n\n        public override void Initialize()\n        {\n            Name = \"DarkeningPanel\";\n\n            SetPositionAndSize();\n\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            DrawBorders = false;\n\n            base.Initialize();\n        }\n\n        public void SetPositionAndSize()\n        {\n            if (Parent != null)\n            {\n                ClientRectangle = new Rectangle(-Parent.X, -Parent.Y,\n                    WindowManager.RenderResolutionX,\n                    WindowManager.RenderResolutionY);\n            }\n            else\n            {\n                ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY);\n            }\n        }\n\n        public override void AddChild(XNAControl child)\n        {\n            base.AddChild(child);\n\n            child.VisibleChanged += Child_VisibleChanged;\n        }\n\n        private void Child_VisibleChanged(object sender, EventArgs e)\n        {\n            var xnaControl = (XNAControl)sender;\n\n            if (xnaControl.Visible)\n                Show();\n            else\n                Hide();\n        }\n\n        public void Show()\n        {\n            Enabled = true;\n            Visible = true;\n\n            if (_fadeEnabled)\n            {\n                AlphaRate = ALPHA_RATE;\n                Alpha = 0.01f;\n            }\n            else\n            {\n                AlphaRate = 1.0f;\n                Alpha = 1.0f;\n            }\n\n            foreach (XNAControl child in Children)\n            {\n                child.Enabled = true;\n                child.Visible = true;\n            }\n        }\n\n        public void Hide()\n        {\n            if (_fadeEnabled)\n            {\n                AlphaRate = -ALPHA_RATE;\n            }\n            else\n            {\n                Enabled = false;\n                Visible = false;\n                Hidden?.Invoke(this, EventArgs.Empty);\n            }\n\n            foreach (XNAControl child in Children)\n            {\n                child.Enabled = false;\n                child.Visible = false;\n            }\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            if (Alpha <= 0.0f)\n            {\n                Enabled = false;\n                Visible = false;\n                Hidden?.Invoke(this, EventArgs.Empty);\n            }\n        }\n\n        public static void AddAndInitializeWithControl(WindowManager wm, XNAControl control)\n        {\n            var dp = new DarkeningPanel(wm);\n            wm.AddAndInitializeControl(dp);\n            dp.AddChild(control);\n        }\n\n        public void ToggleFade(bool enabled)\n        {\n            _fadeEnabled = enabled;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/GameProcessLogic.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing ClientCore;\nusing Rampastring.Tools;\nusing ClientCore.INIProcessing;\nusing System.Threading;\nusing Rampastring.XNAUI;\nusing ClientCore.Extensions;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A static class used for controlling the launching and exiting of the game executable.\n    /// </summary>\n    public static class GameProcessLogic\n    {\n        public static event Action GameProcessStarted;\n\n        public static event Action GameProcessStarting;\n\n        public static event Action GameProcessExited;\n\n        public static bool UseQres { get; set; }\n        public static bool SingleCoreAffinity { get; set; }\n\n        /// <summary>\n        /// Starts the main game process.\n        /// </summary>\n        public static void StartGameProcess(WindowManager windowManager)\n        {\n            Logger.Log(\"About to launch main game executable.\");\n\n            // In the relatively unlikely event that INI preprocessing is still going on, just wait until it's done.\n            // TODO ideally this should be handled in the UI so the client doesn't appear just frozen for the user.\n            int waitTimes = 0;\n            while (PreprocessorBackgroundTask.Instance.IsRunning)\n            {\n                Logger.Log(\"The preprocessor background task is still running. Wait for it...\");\n                Thread.Sleep(1000);\n                waitTimes++;\n                if (waitTimes > 10)\n                {\n                    XNAMessageBox.Show(windowManager, \n                        \"INI preprocessing not complete\".L10N(\"Client:ClientGUI:INIPreprocessingNotCompleteTitle\"),\n                        (\"INI preprocessing not complete. Please try \" +\n                        \"launching the game again. If the problem persists, \" +\n                        \"contact the game or mod authors for support.\").L10N(\"Client:ClientGUI:INIPreprocessingNotCompleteText\"));\n                    return;\n                }\n            }\n\n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n\n            string gameExecutableName;\n            string additionalExecutableName = string.Empty;\n\n            string errorLaunchingTitle = \"Error launching game\".L10N(\"Client:ClientGUI:ErrorLaunchingTitle\");\n            string errorLaunchingText = (\"Error launching {0}. Please check that your anti-virus isn't blocking the CnCNet Client. \" +\n                        \"You can also try running the client as an administrator.\\n\\nYou are unable to participate in this match. \\n\\n\" +\n                        \"Returned error: {1}\").L10N(\"Client:ClientGUI:ErrorLaunchingText\");\n\n            if (osVersion == OSVersion.UNIX)\n                gameExecutableName = ClientConfiguration.Instance.UnixGameExecutableName;\n            else\n            {\n                string launcherExecutableName = ClientConfiguration.Instance.GameLauncherExecutableName;\n                if (string.IsNullOrEmpty(launcherExecutableName))\n                    gameExecutableName = ClientConfiguration.Instance.GetGameExecutableName();\n                else\n                {\n                    gameExecutableName = launcherExecutableName;\n                    additionalExecutableName = \"\\\"\" + ClientConfiguration.Instance.GetGameExecutableName() + \"\\\" \";\n                }\n            }\n\n            string extraCommandLine = ClientConfiguration.Instance.ExtraExeCommandLineParameters;\n\n            SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"DTA.LOG\");\n            SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"TI.LOG\");\n            SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"TS.LOG\");\n\n            GameProcessStarting?.Invoke();\n\n            if (UserINISettings.Instance.WindowedMode && UseQres && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                Logger.Log(\"Windowed mode is enabled - using QRes.\");\n                Process QResProcess = new Process();\n                QResProcess.StartInfo.FileName = ProgramConstants.QRES_EXECUTABLE;\n                QResProcess.StartInfo.UseShellExecute = false;\n\n                if (!string.IsNullOrEmpty(extraCommandLine))\n                    QResProcess.StartInfo.Arguments = \"c=16 /R \" + \"\\\"\" + SafePath.CombineFilePath(ProgramConstants.GamePath, gameExecutableName) + \"\\\" \" + additionalExecutableName + \"-SPAWN \" + extraCommandLine;\n                else\n                    QResProcess.StartInfo.Arguments = \"c=16 /R \" + \"\\\"\" + SafePath.CombineFilePath(ProgramConstants.GamePath, gameExecutableName) + \"\\\" \" + additionalExecutableName + \"-SPAWN\";\n                QResProcess.EnableRaisingEvents = true;\n                QResProcess.Exited += new EventHandler(Process_Exited);\n                Logger.Log(\"Launch executable: \" + QResProcess.StartInfo.FileName);\n                Logger.Log(\"Launch arguments: \" + QResProcess.StartInfo.Arguments);\n                try\n                {\n                    QResProcess.Start();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Error launching QRes: \" + ex.ToString());\n                    XNAMessageBox.Show(windowManager,\n                        errorLaunchingTitle,\n                        string.Format(errorLaunchingText, ProgramConstants.QRES_EXECUTABLE, ex.Message));\n                    Process_Exited(QResProcess, EventArgs.Empty);\n                    return;\n                }\n\n                if (Environment.ProcessorCount > 1 && SingleCoreAffinity)\n                    QResProcess.ProcessorAffinity = (IntPtr)2;\n            }\n            else\n            {\n                string arguments;\n\n                if (!string.IsNullOrWhiteSpace(extraCommandLine))\n                    arguments = \" \" + additionalExecutableName + \"-SPAWN \" + extraCommandLine;\n                else\n                    arguments = additionalExecutableName + \"-SPAWN\";\n\n                FileInfo gameFileInfo = SafePath.GetFile(ProgramConstants.GamePath, gameExecutableName);\n\n                var gameProcess = new Process();\n                gameProcess.StartInfo.FileName = gameFileInfo.FullName;\n                gameProcess.StartInfo.Arguments = arguments;\n                gameProcess.StartInfo.UseShellExecute = false;\n\n                gameProcess.EnableRaisingEvents = true;\n                gameProcess.Exited += Process_Exited;\n\n                Logger.Log(\"Launch executable: \" + gameProcess.StartInfo.FileName);\n                Logger.Log(\"Launch arguments: \" + gameProcess.StartInfo.Arguments);\n                try\n                {\n                    gameProcess.Start();\n                    Logger.Log(\"GameProcessLogic: Process started.\");\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Error launching \" + gameFileInfo.Name + \": \" + ex.ToString());\n                    XNAMessageBox.Show(windowManager,\n                        errorLaunchingTitle,\n                        string.Format(errorLaunchingText, gameFileInfo.Name, ex.Message));\n                    Process_Exited(gameProcess, EventArgs.Empty);\n                    return;\n                }\n\n                if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n                    && Environment.ProcessorCount > 1 && SingleCoreAffinity)\n                {\n                    gameProcess.ProcessorAffinity = (IntPtr)2;\n                }\n            }\n\n            GameProcessStarted?.Invoke();\n\n            Logger.Log(\"Waiting for qres.dat or \" + gameExecutableName + \" to exit.\");\n        }\n\n        static void Process_Exited(object sender, EventArgs e)\n        {\n            Logger.Log(\"GameProcessLogic: Process exited.\");\n            Process proc = (Process)sender;\n            proc.Exited -= Process_Exited;\n            proc.Dispose();\n            GameProcessExited?.Invoke();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/HotkeyConfigurationWindow.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Input;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A window for configuring in-game hotkeys.\n    /// </summary>\n    public class HotkeyConfigurationWindow : XNAWindow\n    {\n        private readonly string HOTKEY_TIP_TEXT = \"Press a key...\".L10N(\"Client:DTAConfig:PressAKey\");\n        private const string KEYBOARD_COMMANDS_INI = \"KeyboardCommands.ini\";\n\n        public HotkeyConfigurationWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        /// <summary>\n        /// Keys that the client doesn't allow to be used regular hotkeys.\n        /// </summary>\n        private readonly Keys[] keyBlacklist = new Keys[]\n        {\n            Keys.LeftAlt,\n            Keys.RightAlt,\n            Keys.LeftControl,\n            Keys.RightControl,\n            Keys.LeftShift,\n            Keys.RightShift\n        };\n\n        private readonly List<GameCommand> gameCommands = new List<GameCommand>();\n\n        private XNAClientDropDown ddCategory = null!;\n        private XNAMultiColumnListBox lbHotkeys = null!;\n\n        private XNAPanel hotkeyInfoPanel = null!;\n        private XNALabel lblCommandCaption = null!;\n        private XNALabel lblDescription = null!;\n        private XNALabel lblCurrentHotkeyValue = null!;\n        private XNALabel lblNewHotkeyValue = null!;\n        private XNALabel lblCurrentlyAssignedTo = null!;\n\n        private XNALabel lblDefaultHotkeyValue = null!;\n        private XNAClientButton btnResetKey = null!;\n\n        private Hotkey pendingHotkey = Hotkey.None;\n        private KeyModifiers lastFrameModifiers;\n\n        public override void Initialize()\n        {\n            ReadGameCommands();\n\n            Name = \"HotkeyConfigurationWindow\";\n            ClientRectangle = new Rectangle(0, 0, 600, 450);\n            BackgroundTexture = AssetLoader.LoadTextureUncached(\"hotkeyconfigbg.png\");\n\n            var lblCategory = new XNALabel(WindowManager);\n            lblCategory.Name = \"lblCategory\";\n            lblCategory.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            lblCategory.Text = \"Category:\".L10N(\"Client:DTAConfig:Category\");\n\n            ddCategory = new XNAClientDropDown(WindowManager);\n            ddCategory.Name = \"ddCategory\";\n            ddCategory.ClientRectangle = new Rectangle(lblCategory.Right + 12,\n                lblCategory.Y - 1, 250, ddCategory.Height);\n\n            HashSet<string> categories = new HashSet<string>();\n\n            foreach (var command in gameCommands)\n            {\n                if (!categories.Contains(command.Category))\n                    categories.Add(command.Category);\n            }\n\n            foreach (string category in categories)\n                ddCategory.AddItem(category);\n\n            lbHotkeys = new XNAMultiColumnListBox(WindowManager);\n            lbHotkeys.Name = \"lbHotkeys\";\n            lbHotkeys.ClientRectangle = new Rectangle(12, ddCategory.Bottom + 12,\n                ddCategory.Right - 12, Height - ddCategory.Bottom - 59);\n            lbHotkeys.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbHotkeys.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbHotkeys.AddColumn(\"Command\".L10N(\"Client:DTAConfig:Command\"), 150);\n            lbHotkeys.AddColumn(\"Shortcut\".L10N(\"Client:DTAConfig:Shortcut\"), lbHotkeys.Width - 150);\n\n            hotkeyInfoPanel = new XNAPanel(WindowManager);\n            hotkeyInfoPanel.Name = \"HotkeyInfoPanel\";\n            hotkeyInfoPanel.ClientRectangle = new Rectangle(lbHotkeys.Right + 12,\n                ddCategory.Y, Width - lbHotkeys.Right - 24, lbHotkeys.Height + ddCategory.Height + 12);\n\n            lblCommandCaption = new XNALabel(WindowManager);\n            lblCommandCaption.Name = \"lblCommandCaption\";\n            lblCommandCaption.FontIndex = 1;\n            lblCommandCaption.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            lblCommandCaption.Text = \"Command name\".L10N(\"Client:DTAConfig:CommandName\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = \"lblDescription\";\n            lblDescription.ClientRectangle = new Rectangle(12, lblCommandCaption.Bottom + 12, 0, 0);\n            lblDescription.Text = \"Command description\".L10N(\"Client:DTAConfig:CommandDescription\");\n\n            var lblCurrentHotkey = new XNALabel(WindowManager);\n            lblCurrentHotkey.Name = \"lblCurrentHotkey\";\n            lblCurrentHotkey.ClientRectangle = new Rectangle(lblDescription.X,\n                lblDescription.Bottom + 48, 0, 0);\n            lblCurrentHotkey.FontIndex = 1;\n            lblCurrentHotkey.Text = \"Currently assigned hotkey:\".L10N(\"Client:DTAConfig:CurrentHotKey\");\n\n            lblCurrentHotkeyValue = new XNALabel(WindowManager);\n            lblCurrentHotkeyValue.Name = \"lblCurrentHotkeyValue\";\n            lblCurrentHotkeyValue.ClientRectangle = new Rectangle(lblDescription.X,\n                lblCurrentHotkey.Bottom + 6, 0, 0);\n            lblCurrentHotkeyValue.Text = \"Current hotkey value\".L10N(\"Client:DTAConfig:CurrentHotKeyValue\");\n\n            var lblNewHotkey = new XNALabel(WindowManager);\n            lblNewHotkey.Name = \"lblNewHotkey\";\n            lblNewHotkey.ClientRectangle = new Rectangle(lblDescription.X,\n                lblCurrentHotkeyValue.Bottom + 48, 0, 0);\n            lblNewHotkey.FontIndex = 1;\n            lblNewHotkey.Text = \"New hotkey:\".L10N(\"Client:DTAConfig:NewHotKey\");\n\n            lblNewHotkeyValue = new XNALabel(WindowManager);\n            lblNewHotkeyValue.Name = \"lblNewHotkeyValue\";\n            lblNewHotkeyValue.ClientRectangle = new Rectangle(lblDescription.X,\n                lblNewHotkey.Bottom + 6, 0, 0);\n            lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT;\n\n            lblCurrentlyAssignedTo = new XNALabel(WindowManager);\n            lblCurrentlyAssignedTo.Name = \"lblCurrentlyAssignedTo\";\n            lblCurrentlyAssignedTo.ClientRectangle = new Rectangle(lblDescription.X,\n                lblNewHotkeyValue.Bottom + 12, 0, 0);\n            lblCurrentlyAssignedTo.Text = \"Currently assigned to:\".L10N(\"Client:DTAConfig:CurrentHotKeyAssign\") + \"\\nKey\";\n\n            var btnAssign = new XNAClientButton(WindowManager);\n            btnAssign.Name = \"btnAssign\";\n            btnAssign.ClientRectangle = new Rectangle(lblDescription.X,\n                lblCurrentlyAssignedTo.Bottom + 24, UIDesignConstants.BUTTON_WIDTH_121, UIDesignConstants.BUTTON_HEIGHT);\n            btnAssign.Text = \"Assign Hotkey\".L10N(\"Client:DTAConfig:AssignHotkey\");\n            btnAssign.LeftClick += BtnAssign_LeftClick;\n\n            btnResetKey = new XNAClientButton(WindowManager);\n            btnResetKey.Name = \"btnResetKey\";\n            btnResetKey.ClientRectangle = new Rectangle(btnAssign.X, btnAssign.Bottom + 12, btnAssign.Width, 23);\n            btnResetKey.Text = \"Reset to Default\".L10N(\"Client:DTAConfig:ResetToDefault\");\n            btnResetKey.LeftClick += BtnReset_LeftClick;\n\n            var lblDefaultHotkey = new XNALabel(WindowManager);\n            lblDefaultHotkey.Name = \"lblOriginalHotkey\";\n            lblDefaultHotkey.ClientRectangle = new Rectangle(lblCurrentHotkey.X, btnResetKey.Bottom + 12, 0, 0);\n            lblDefaultHotkey.Text = \"Default hotkey:\".L10N(\"Client:DTAConfig:DefaultHotKey\");\n\n            lblDefaultHotkeyValue = new XNALabel(WindowManager);\n            lblDefaultHotkeyValue.Name = \"lblDefaultHotkeyValue\";\n            lblDefaultHotkeyValue.ClientRectangle = new Rectangle(lblDefaultHotkey.Right + 12, lblDefaultHotkey.Y, 0, 0);\n\n            var btnSave = new XNAClientButton(WindowManager);\n            btnSave.Name = \"btnSave\";\n            btnSave.ClientRectangle = new Rectangle(12, lbHotkeys.Bottom + 12, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnSave.Text = \"Save\".L10N(\"Client:DTAConfig:ButtonSave\");\n            btnSave.LeftClick += BtnSave_LeftClick;\n\n            var btnResetAllKeys = new XNAClientButton(WindowManager);\n            btnResetAllKeys.Name = \"btnResetAllToDefaults\";\n            btnResetAllKeys.ClientRectangle = new Rectangle(0, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_121, UIDesignConstants.BUTTON_HEIGHT);\n            btnResetAllKeys.Text = \"Reset All Keys\".L10N(\"Client:DTAConfig:ResetAllHotkey\");\n            btnResetAllKeys.LeftClick += BtnResetToDefaults_LeftClick;\n            AddChild(btnResetAllKeys);\n            btnResetAllKeys.CenterOnParentHorizontally();\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = \"btnExit\";\n            btnCancel.ClientRectangle = new Rectangle(Width - 104, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:DTAConfig:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(lbHotkeys);\n            AddChild(lblCategory);\n            AddChild(ddCategory);\n            AddChild(hotkeyInfoPanel);\n            AddChild(btnSave);\n            AddChild(btnCancel);\n            hotkeyInfoPanel.AddChild(lblCommandCaption);\n            hotkeyInfoPanel.AddChild(lblDescription);\n            hotkeyInfoPanel.AddChild(lblCurrentHotkey);\n            hotkeyInfoPanel.AddChild(lblCurrentHotkeyValue);\n            hotkeyInfoPanel.AddChild(lblNewHotkey);\n            hotkeyInfoPanel.AddChild(lblNewHotkeyValue);\n            hotkeyInfoPanel.AddChild(lblCurrentlyAssignedTo);\n            hotkeyInfoPanel.AddChild(lblDefaultHotkey);\n            hotkeyInfoPanel.AddChild(lblDefaultHotkeyValue);\n            hotkeyInfoPanel.AddChild(btnAssign);\n            hotkeyInfoPanel.AddChild(btnResetKey);\n\n            if (categories.Count > 0)\n            {\n                hotkeyInfoPanel.Disable();\n                lbHotkeys.SelectedIndexChanged += LbHotkeys_SelectedIndexChanged;\n\n                ddCategory.SelectedIndexChanged += DdCategory_SelectedIndexChanged;\n                ddCategory.SelectedIndex = 0;\n            }\n            else\n                Logger.Log(\"No keyboard game commands exist!\");\n\n            GameProcessLogic.GameProcessExited += GameProcessLogic_GameProcessExited;\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            Keyboard.OnKeyPressed += Keyboard_OnKeyPressed;\n            EnabledChanged += HotkeyConfigurationWindow_EnabledChanged;\n\n            // Load and apply the hotkeys so that if the default keyboard INI file is updated during a client update\n            LoadKeyboardINI();\n            RefreshHotkeyList();\n            WriteKeyboardINI(writeEvenIfSettingsIniAsKeyboardIniHolds: true);\n        }\n\n        /// <summary>\n        /// Reads game commands from an INI file.\n        /// </summary>\n        private void ReadGameCommands()\n        {\n            var gameCommandsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), KEYBOARD_COMMANDS_INI));\n\n            List<string> sections = gameCommandsIni.GetSections();\n\n            HashSet<Hotkey> defaultHotkeys = [];\n\n            foreach (string sectionName in sections)\n            {\n                var gameCommand = new GameCommand(gameCommandsIni.GetSection(sectionName));\n                gameCommands.Add(gameCommand);\n\n                // Check duplicates for default hotkeys\n                if (gameCommand.DefaultHotkey != null && gameCommand.DefaultHotkey != Hotkey.None)\n                {\n                    bool isDuplicate = !defaultHotkeys.Add(gameCommand.DefaultHotkey);\n\n                    if (isDuplicate)\n                        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.\");\n                }\n            }\n        }\n\n        /// <summary>\n        /// Resets the hotkey for the currently selected game command to its\n        /// default value.\n        /// </summary>\n        private void BtnReset_LeftClick(object? sender, EventArgs e)\n        {\n            if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount)\n            {\n                return;\n            }\n\n            var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag;\n\n            if (command.DefaultHotkey == null)\n            {\n                command.Hotkey = null;\n            }\n            else\n            {\n                command.Hotkey = command.DefaultHotkey;\n\n                // If the hotkey is already assigned to some other command, unbind it\n                foreach (var gameCommand in gameCommands)\n                {\n                    if (gameCommand != command && gameCommand.Hotkey == command.Hotkey)\n                        gameCommand.Hotkey = null;\n                }\n            }\n\n            pendingHotkey = Hotkey.None;\n            RefreshHotkeyList();\n        }\n\n        private void BtnResetToDefaults_LeftClick(object? sender, EventArgs e)\n        {\n            foreach (var command in gameCommands)\n            {\n                if (command.DefaultHotkey == null)\n                    command.Hotkey = null;\n                else\n                    command.Hotkey = command.DefaultHotkey;\n            }\n\n            RefreshHotkeyList();\n        }\n\n        private void HotkeyConfigurationWindow_EnabledChanged(object? sender, EventArgs e)\n        {\n            if (Enabled)\n            {\n                LoadKeyboardINI();\n                RefreshHotkeyList();\n            }\n        }\n\n        /// <summary>\n        /// Reloads Keyboard.ini when the game process has exited.\n        /// </summary>\n        private void GameProcessLogic_GameProcessExited()\n        {\n            WindowManager.AddCallback(new Action(LoadKeyboardINI), null);\n        }\n\n        private void LoadKeyboardINI()\n        {\n            var keyboardINI = ClientConfiguration.Instance.SettingsIniAsKeyboardIni\n                ? UserINISettings.Instance.SettingsIni\n                : new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI));\n            var hotkeySection = keyboardINI.GetOrAddSection(ClientConfiguration.Instance.KeyboardHotkeySection);\n\n            // Load the hotkeys from the INI file\n            var assignedHotkeys = new HashSet<Hotkey>();\n            foreach (var command in gameCommands)\n            {\n                int? tsHotkey = hotkeySection.GetIntValueOrNull(command.ININame);\n\n                if (tsHotkey.HasValue)\n                {\n                    Hotkey hotkey = new(tsHotkey.Value);\n                    bool isDuplicate = false;\n                    if (hotkey != Hotkey.None)\n                        isDuplicate = !assignedHotkeys.Add(hotkey);\n\n                    if (!isDuplicate)\n                        command.Hotkey = hotkey;\n                    else\n                        command.Hotkey = null;\n                }\n                else\n                {\n                    // Clear any previously assigned hotkey when no value exists in the INI\n                    command.Hotkey = null;\n                }\n            }\n\n            // Assign default hotkeys\n            foreach (var command in gameCommands)\n            {\n                bool hotkeyAssigned = hotkeySection.KeyExists(command.ININame);\n\n                if (!hotkeyAssigned && command.DefaultHotkey != null)\n                {\n                    // Try assigning the default hotkey if it exists and is not occupied by other commands\n                    bool occupied = false;\n                    if (command.DefaultHotkey != Hotkey.None)\n                    {\n                        foreach (var otherCommand in gameCommands)\n                        {\n                            if (otherCommand != command && command.DefaultHotkey == otherCommand.Hotkey)\n                            {\n                                occupied = true;\n                                break;\n                            }\n                        }\n                    }\n\n                    if (!occupied)\n                        command.Hotkey = command.DefaultHotkey;\n                }\n            }\n        }\n\n        private void LbHotkeys_SelectedIndexChanged(object? sender, EventArgs e)\n        {\n            if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount)\n            {\n                hotkeyInfoPanel.Disable();\n                return;\n            }\n\n            hotkeyInfoPanel.Enable();\n            var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag;\n            lblCommandCaption.Text = command.UIName;\n            lblDescription.Text = Renderer.FixText(command.Description, lblDescription.FontIndex,\n                hotkeyInfoPanel.Width - lblDescription.X).Text;\n            lblCurrentHotkeyValue.Text = command.Hotkey?.ToStringWithNone();\n\n            lblDefaultHotkeyValue.Text = command.DefaultHotkey?.ToStringWithNone();\n            btnResetKey.Enabled = command.DefaultHotkey != command.Hotkey;\n\n            lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT;\n            pendingHotkey = Hotkey.None;\n            lblCurrentlyAssignedTo.Text = string.Empty;\n        }\n\n        private void DdCategory_SelectedIndexChanged(object? sender, EventArgs e)\n        {\n            lbHotkeys.ClearItems();\n            lbHotkeys.TopIndex = 0;\n            string category = ddCategory.SelectedItem.Text;\n            foreach (var command in gameCommands)\n            {\n                if (command.Category == category)\n                {\n                    lbHotkeys.AddItem(new XNAListBoxItem[] {\n                        new XNAListBoxItem() { Text = command.UIName, Tag = command },\n                        new XNAListBoxItem() { Text = command.Hotkey?.ToString() }\n                    });\n                }\n            }\n\n            lbHotkeys.SelectedIndex = -1;\n        }\n\n        private void BtnAssign_LeftClick(object? sender, EventArgs e)\n        {\n            if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount)\n            {\n                return;\n            }\n\n            // If the hotkey is already assigned to other command, unbind it\n            if (pendingHotkey != Hotkey.None)\n            {\n                foreach (var gameCommand in gameCommands)\n                {\n                    if (pendingHotkey == gameCommand.Hotkey)\n                        gameCommand.Hotkey = null;\n                }\n            }\n\n            var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag;\n            command.Hotkey = pendingHotkey;\n            RefreshHotkeyList();\n            pendingHotkey = Hotkey.None;\n        }\n\n        private void RefreshHotkeyList()\n        {\n            int selectedIndex = lbHotkeys.SelectedIndex;\n            int topIndex = lbHotkeys.TopIndex;\n            DdCategory_SelectedIndexChanged(null, EventArgs.Empty);\n            lbHotkeys.TopIndex = topIndex;\n            lbHotkeys.SelectedIndex = selectedIndex;\n        }\n\n        /// <summary>\n        /// Detects when the user has pressed a key to generate a new hotkey.\n        /// </summary>\n        private void Keyboard_OnKeyPressed(object? sender, Rampastring.XNAUI.Input.KeyPressEventArgs e)\n        {\n            foreach (var blacklistedKey in keyBlacklist)\n            {\n                if (e.PressedKey == blacklistedKey)\n                    return;\n            }\n\n            var currentModifiers = GetCurrentModifiers();\n\n            // The XNA keys seem to match the Windows virtual keycodes! This saves us some work\n            pendingHotkey = new Hotkey(e.PressedKey, currentModifiers);\n\n            lblCurrentlyAssignedTo.Text = string.Empty;\n\n            foreach (var command in gameCommands)\n            {\n                if (pendingHotkey == command.Hotkey)\n                    lblCurrentlyAssignedTo.Text = \"Currently assigned to:\".L10N(\"Client:DTAConfig:CurrentAssignTo\") + Environment.NewLine + command.UIName;\n            }\n        }\n\n        private void BtnCancel_LeftClick(object? sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        private void BtnSave_LeftClick(object? sender, EventArgs e)\n        {\n            WriteKeyboardINI();\n            Disable();\n        }\n\n        /// <summary>\n        /// Updates the logic of the window.\n        /// Used for keeping the \"new hotkey\" display in sync with the keyboard's\n        /// modifier keys.\n        /// </summary>\n        /// <param name=\"gameTime\">Provides a snapshot of timing values.</param>\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            var oldModifiers = pendingHotkey.Modifier;\n            var currentModifiers = GetCurrentModifiers();\n\n            if ((pendingHotkey.Key == Keys.None && currentModifiers != oldModifiers)\n                ||\n                (pendingHotkey.Key != Keys.None &&\n                lastFrameModifiers == KeyModifiers.None &&\n                currentModifiers != lastFrameModifiers))\n            {\n                pendingHotkey = new Hotkey(Keys.None, currentModifiers);\n                lblCurrentlyAssignedTo.Text = string.Empty;\n            }\n\n            string displayString = pendingHotkey.ToString();\n            if (displayString != string.Empty)\n                lblNewHotkeyValue.Text = pendingHotkey.ToString();\n            else\n                lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT;\n\n            lastFrameModifiers = currentModifiers;\n        }\n\n        /// <summary>\n        /// Detects which key modifiers (Ctrl, Shift, Alt) the user is currently pressing.\n        /// </summary>\n        private KeyModifiers GetCurrentModifiers()\n        {\n            var currentModifiers = KeyModifiers.None;\n\n            if (Keyboard.IsKeyHeldDown(Keys.RightControl) ||\n                Keyboard.IsKeyHeldDown(Keys.LeftControl))\n            {\n                currentModifiers |= KeyModifiers.Ctrl;\n            }\n\n            if (Keyboard.IsKeyHeldDown(Keys.RightShift) ||\n                Keyboard.IsKeyHeldDown(Keys.LeftShift))\n            {\n                currentModifiers |= KeyModifiers.Shift;\n            }\n\n            if (Keyboard.IsKeyHeldDown(Keys.LeftAlt) ||\n                Keyboard.IsKeyHeldDown(Keys.RightAlt))\n            {\n                currentModifiers |= KeyModifiers.Alt;\n            }\n\n            return currentModifiers;\n        }\n\n        private bool HasDuplicateHotkeys()\n        {\n            var assignedHotkeys = new HashSet<Hotkey>();\n            foreach (var command in gameCommands)\n            {\n                if (command.Hotkey != null && command.Hotkey != Hotkey.None)\n                {\n                    if (assignedHotkeys.Contains(command.Hotkey))\n                    {\n#if DEBUG\n                        Debugger.Break();\n#endif\n\n                        return true;\n                    }\n\n                    assignedHotkeys.Add(command.Hotkey);\n                }\n            }\n\n            return false;\n        }\n\n        private void WriteKeyboardINI(bool writeEvenIfSettingsIniAsKeyboardIniHolds = false)\n        {\n            Debug.Assert(!HasDuplicateHotkeys(), \"There are duplicate hotkeys assigned. How could this happen?\");\n\n            IniFile keyboardIni = ClientConfiguration.Instance.SettingsIniAsKeyboardIni\n                    ? UserINISettings.Instance.SettingsIni\n                    : new IniFile() { FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI) };\n\n            var hotkeySection = keyboardIni.GetOrAddSection(ClientConfiguration.Instance.KeyboardHotkeySection);\n            foreach (var command in gameCommands)\n            {\n                // Note: we now explicitly differentiate between null and Hotkey.None\n                if (command.Hotkey == null)\n                {\n                    if (hotkeySection.KeyExists(command.ININame))\n                        hotkeySection.RemoveKey(command.ININame);\n                }\n                else\n                {\n                    hotkeySection.SetStringValue(command.ININame, command.Hotkey.GetTSEncoded().ToString());\n                }\n            }\n\n            // Do not write INI file if using Settings.ini as Keyboard.ini. The hot keys will be saved when Settings.ini is saved.\n            // 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.\n            // If we don't flush here, everything can be restored by hitting a cancel.\n            // If we flush here -- the player can't cancel anymore at all.\n            if (writeEvenIfSettingsIniAsKeyboardIniHolds || !ClientConfiguration.Instance.SettingsIniAsKeyboardIni)\n                keyboardIni.WriteIniFile();\n        }\n\n        /// <summary>\n        /// A game command that can be assigned into a key on the keyboard.\n        /// </summary>\n        private class GameCommand\n        {\n            public GameCommand(string uiName, string category, string description, string iniName)\n            {\n                UIName = uiName;\n                Category = category;\n                Description = description;\n                ININame = iniName;\n            }\n\n            /// <summary>\n            /// Creates a game command and parses its information from an INI section.\n            /// </summary>\n            /// <param name=\"iniSection\">The INI section.</param>\n            public GameCommand(IniSection iniSection)\n            {\n                ININame = iniSection.SectionName;\n                UIName = iniSection.GetStringValue(\"UIName\", \"Unnamed command\")\n                    .L10N($\"INI:Hotkeys:{ININame}:UIName\");\n                string category = iniSection.GetStringValue(\"Category\", \"Unknown category\");\n                Category = category.L10N($\"INI:HotkeyCategories:{category}\");\n                Description = iniSection.GetStringValue(\"Description\", \"Unknown description\")\n                    .L10N($\"INI:Hotkeys:{ININame}:Description\");\n\n                int? defaultTSKey = iniSection.GetIntValueOrNull(\"DefaultKey\");\n                DefaultHotkey = defaultTSKey.HasValue ? new Hotkey(defaultTSKey.Value) : null;\n\n                // 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\n                if (DefaultHotkey == Hotkey.None)\n                    DefaultHotkey = null;\n            }\n\n            public string UIName { get; private set; }\n            public string Category { get; private set; }\n            public string Description { get; private set; }\n            public string ININame { get; private set; }\n            public Hotkey? Hotkey { get; set; }\n            public Hotkey? DefaultHotkey { get; private set; }\n        }\n\n        [Flags]\n        private enum KeyModifiers\n        {\n            None = 0,\n            Shift = 1,\n            Ctrl = 2,\n            Alt = 4\n        }\n\n        /// <summary>\n        /// Represents a keyboard key with modifiers.\n        /// </summary>\n        private sealed record Hotkey\n        {\n            public Keys Key { get; }\n            public KeyModifiers Modifier { get; }\n\n            public static readonly Hotkey None = new(Keys.None, KeyModifiers.None);\n\n            /// <summary>\n            /// Creates a new hotkey by decoding a Tiberian Sun / Red Alert 2\n            /// encoded key value.\n            /// </summary>\n            /// <param name=\"encodedKeyValue\">The encoded key value.</param>\n            public Hotkey(int encodedKeyValue)\n            {\n                Key = (Keys)(encodedKeyValue & 255);\n                Modifier = (KeyModifiers)(encodedKeyValue >> 8);\n            }\n\n            public Hotkey(Keys key, KeyModifiers modifiers)\n            {\n                Key = key;\n                Modifier = modifiers;\n            }\n\n            public override string ToString()\n            {\n                if (Key == Keys.None && Modifier == KeyModifiers.None)\n                    return string.Empty;\n\n                return GetString();\n            }\n\n            public string ToStringWithNone()\n            {\n                if (Key == Keys.None && Modifier == KeyModifiers.None)\n                    return \"None\".L10N(\"Client:DTAConfig:HotkeyNone\");\n\n                return GetString();\n            }\n\n            /// <summary>\n            /// Creates the display string for this key.\n            /// </summary>\n            private string GetString()\n            {\n                string str = \"\";\n\n                if (Modifier.HasFlag(KeyModifiers.Shift))\n                    str += \"SHIFT+\";\n\n                if (Modifier.HasFlag(KeyModifiers.Ctrl))\n                    str += \"CTRL+\";\n\n                if (Modifier.HasFlag(KeyModifiers.Alt))\n                    str += \"ALT+\";\n\n                if (Key == Keys.None)\n                    return str;\n\n                return str + GetKeyDisplayString(Key);\n            }\n\n            /// <summary>\n            /// Returns the hotkey in the Tiberian Sun / Red Alert 2 Keyboard.ini encoded format.\n            /// </summary>\n            public int GetTSEncoded()\n            {\n                return ((int)Modifier << 8) + (int)Key;\n            }\n\n            /// <summary>\n            /// Returns the display string for an XNA key.\n            /// Allows overriding specific key enum names to be more\n            /// suitable for the UI.\n            /// </summary>\n            /// <param name=\"key\">The key.</param>\n            /// <returns>A string.</returns>\n            private string GetKeyDisplayString(Keys key)\n            {\n                switch (key)\n                {\n                    case Keys.D0:\n                        return \"0\";\n                    case Keys.D1:\n                        return \"1\";\n                    case Keys.D2:\n                        return \"2\";\n                    case Keys.D3:\n                        return \"3\";\n                    case Keys.D4:\n                        return \"4\";\n                    case Keys.D5:\n                        return \"5\";\n                    case Keys.D6:\n                        return \"6\";\n                    case Keys.D7:\n                        return \"7\";\n                    case Keys.D8:\n                        return \"8\";\n                    case Keys.D9:\n                        return \"9\";\n                    case (Keys)12:\n                        return \"NumPad5 (NumLock off)\";\n                    case (Keys)0x10:\n                        return \"Shift\";\n                    case (Keys)0x11:\n                        return \"Ctrl\";\n                    case (Keys)0x12:\n                        return \"Alt\";\n                    default:\n                        return key.ToString();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/ICompositeControl.cs",
    "content": "﻿using System.Collections.Generic;\n\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI;\n\n/// <summary>\n/// Indicates that the implementer has sub-controls that need to be exposed to INI system.\n/// </summary>\n/// <remarks>\n/// Currently only supported in <see cref=\"INItializableWindow\">.\n/// </remarks>\npublic interface ICompositeControl\n{\n    /// <summary>\n    /// The sub-controls that are exposed to the INI system.\n    /// </summary>\n    /// <remarks>\n    /// All the sub-controls should have their names set to something\n    /// unique to each composite control. Utilise <see cref=\"XNAControl.NameChanged\"/>\n    /// event to set the names of the sub-controls.\n    /// </remarks>\n    IReadOnlyList<XNAControl> SubControls { get; }\n}"
  },
  {
    "path": "ClientGUI/IME/DummyIMEHandler.cs",
    "content": "﻿#nullable enable\nusing Microsoft.Xna.Framework;\n\nnamespace ClientGUI.IME\n{\n    internal class DummyIMEHandler : IMEHandler\n    {\n        public DummyIMEHandler() { }\n\n        public override bool TextCompositionEnabled { get => false; protected set { } }\n\n        public override void SetTextInputRectangle(Rectangle rectangle) { }\n        public override void StartTextComposition() { }\n        public override void StopTextComposition() { }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/IME/IMEHandler.cs",
    "content": "#nullable enable\nusing System;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.Input;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI.IME;\n\npublic abstract class IMEHandler : IIMEHandler\n{\n    bool IIMEHandler.TextCompositionEnabled => TextCompositionEnabled;\n    public abstract bool TextCompositionEnabled { get; protected set; }\n\n    private XNATextBox? _IMEFocus = null;\n    public XNATextBox? IMEFocus\n    {\n        get => _IMEFocus;\n        protected set\n        {\n            _IMEFocus = value;\n            Debug.Assert(!_IMEFocus?.IMEDisabled ?? true, \"IME focus should not be assigned from a textbox with IME disabled\");\n        }\n    }\n\n    private string _composition = string.Empty;\n\n    public string Composition\n    {\n        get => _composition;\n        protected set\n        {\n            string old = _composition;\n            _composition = value;\n            OnCompositionChanged(old, value);\n        }\n    }\n\n    public bool CompositionEmpty => string.IsNullOrEmpty(_composition);\n\n    /// <summary>\n    /// Indicates whether an IME event has been received ever. Used to distinguish IME users from non-IME users.\n    /// </summary>\n    protected bool IMEEventReceived = false;\n\n    protected bool LastActionIMEChatInput = true;\n\n    private void OnCompositionChanged(string oldValue, string newValue)\n    {\n        //Debug.WriteLine($\"IME: OnCompositionChanged: {newValue.Length - oldValue.Length}\");\n\n        IMEEventReceived = true;\n        // It seems that OnIMETextInput() is always triggered after OnCompositionChanged(). We expect such a behavior.\n        LastActionIMEChatInput = false;\n    }\n\n    protected ConcurrentDictionary<XNATextBox, Action<char>?> TextBoxHandleChatInputCallbacks = [];\n\n    public virtual int CompositionCursorPosition { get; set; }\n\n    public static IMEHandler Create(Game game)\n    {\n#if DX\n        return new WinFormsIMEHandler(game);\n#elif XNA\n        // Warning: Think carefully before enabling WinFormsIMEHandler for XNA builds!\n        // It *might* occasionally crash due to an unknown stack overflow issue.\n        // This *might* be caused by both ImeSharp and XNAUI hooking into WndProc.\n        // ImeSharp: https://github.com/ryancheung/ImeSharp/blob/dc2243beff9ef48eb37e398c506c905c965f8e68/ImeSharp/InputMethod.cs#L170\n        // XNAUI: https://github.com/Rampastring/Rampastring.XNAUI/blob/9a7d5bb3e47ea50286ee05073d0a6723bc6d764d/Input/KeyboardEventInput.cs#L79\n        //\n        // That said, you can try returning a WinFormsIMEHandler and test if it is stable enough now. Who knows?\n        return new DummyIMEHandler();\n#elif GL\n        return new SdlIMEHandler(game);\n#else\n#error Unknown variant\n#endif\n    }\n\n    public abstract void SetTextInputRectangle(Rectangle rectangle);\n\n    public abstract void StartTextComposition();\n\n    public abstract void StopTextComposition();\n\n    protected virtual void OnIMETextInput(char character)\n    {\n        //Debug.WriteLine($\"IME: OnIMETextInput: {character} {(short)character}; IMEFocus is null? {IMEFocus == null}\");\n\n        LastActionIMEChatInput = true;\n\n        if (IMEFocus != null)\n        {\n            TextBoxHandleChatInputCallbacks.TryGetValue(IMEFocus, out var handleChatInput);\n            handleChatInput?.Invoke(character);\n        }\n    }\n\n    public void SetIMETextInputRectangle(WindowManager manager)\n    {\n        // When the client window resizes, we should call SetIMETextInputRectangle()\n        if (manager.SelectedControl is XNATextBox textBox)\n            SetIMETextInputRectangle(textBox);\n    }\n\n    private void SetIMETextInputRectangle(XNATextBox sender)\n    {\n        WindowManager windowManager = sender.WindowManager;\n\n        Rectangle textBoxRect = sender.RenderRectangle();\n        double scaleRatio = windowManager.ScaleRatio;\n\n        Rectangle rect = new()\n        {\n            X = (int)(textBoxRect.X * scaleRatio + windowManager.SceneXPosition),\n            Y = (int)(textBoxRect.Y * scaleRatio + windowManager.SceneYPosition),\n            Width = (int)(textBoxRect.Width * scaleRatio),\n            Height = (int)(textBoxRect.Height * scaleRatio)\n        };\n\n        // The following code returns a more accurate location based on the current InputPosition.\n        // However, as SetIMETextInputRectangle() does not automatically update with changes in InputPosition\n        // (e.g., due to scrolling or mouse clicks altering the textbox's input position without shifting focus),\n        // accuracy becomes inconsistent. Sometimes it's precise, other times it's off,\n        // which is arguably worse than a consistent but manageable inaccuracy.\n        // This inconsistency could lead to a confusing user experience,\n        // as the input rectangle's position may not reliably reflect the current input position.\n        // Therefore, unless whenever InputPosition is changed, SetIMETextInputRectangle() is raised\n        // -- which requires more time to investigate and test, it's commented out for now.\n        //var vec = Renderer.GetTextDimensions(\n        //    sender.Text.Substring(sender.TextStartPosition, sender.InputPosition),\n        //    sender.FontIndex);\n        //rect.X += (int)(vec.X * scaleRatio);\n\n        SetTextInputRectangle(rect);\n    }\n\n    void IIMEHandler.OnSelectedChanged(XNATextBox sender)\n    {\n        if (sender.WindowManager.SelectedControl == sender)\n        {\n            StopTextComposition();\n\n            if (!sender.IMEDisabled && sender.Enabled && sender.Visible)\n            {\n                IMEFocus = sender;\n\n                // Update the location of IME based on the textbox\n                SetIMETextInputRectangle(sender);\n\n                StartTextComposition();\n            }\n            else\n            {\n                IMEFocus = null;\n            }\n        }\n        else if (sender.WindowManager.SelectedControl is not XNATextBox)\n        {\n            // Disable IME since the current selected control is not XNATextBox\n            IMEFocus = null;\n            StopTextComposition();\n        }\n\n        // Note: if sender.WindowManager.SelectedControl != sender and is XNATextBox,\n        // another OnSelectedChanged() will be triggered,\n        // so we do not need to handle this case\n    }\n\n    void IIMEHandler.RegisterXNATextBox(XNATextBox sender, Action<char>? handleCharInput)\n        => TextBoxHandleChatInputCallbacks[sender] = handleCharInput;\n\n    void IIMEHandler.KillXNATextBox(XNATextBox sender)\n        => TextBoxHandleChatInputCallbacks.TryRemove(sender, out _);\n\n    bool IIMEHandler.HandleScrollLeftKey(XNATextBox sender)\n        => !CompositionEmpty;\n\n    bool IIMEHandler.HandleScrollRightKey(XNATextBox sender)\n        => !CompositionEmpty;\n\n    bool IIMEHandler.HandleBackspaceKey(XNATextBox sender)\n    {\n        bool handled = !LastActionIMEChatInput;\n        LastActionIMEChatInput = true;\n        //Debug.WriteLine($\"IME: HandleBackspaceKey: handled: {handled}\");\n        return handled;\n    }\n\n    bool IIMEHandler.HandleDeleteKey(XNATextBox sender)\n    {\n        bool handled = !LastActionIMEChatInput;\n        LastActionIMEChatInput = true;\n        //Debug.WriteLine($\"IME: HandleDeleteKey: handled: {handled}\");\n        return handled;\n    }\n\n    bool IIMEHandler.GetDrawCompositionText(XNATextBox sender, out string composition, out int compositionCursorPosition)\n    {\n        if (IMEFocus != sender || CompositionEmpty)\n        {\n            composition = string.Empty;\n            compositionCursorPosition = 0;\n            return false;\n        }\n\n        composition = Composition;\n        compositionCursorPosition = CompositionCursorPosition;\n        return true;\n    }\n\n    bool IIMEHandler.HandleCharInput(XNATextBox sender, char input)\n        => TextCompositionEnabled;\n\n    bool IIMEHandler.HandleEnterKey(XNATextBox sender)\n        => false;\n\n    bool IIMEHandler.HandleEscapeKey(XNATextBox sender)\n    {\n        //Debug.WriteLine($\"IME: HandleEscapeKey: handled: {IMEEventReceived}\");\n\n        // This method disables the ESC handling of the TextBox as long as the user has ever used IME.\n        // This is because IME users often use ESC to cancel composition. Even if currently the composition is empty,\n        // the user still expects ESC to cancel composition rather than deleting the whole sentence.\n        // 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.\n\n        // Note: \"!CompositionEmpty => IMEEventReceived\" should hold, but just in case\n\n        return IMEEventReceived || !CompositionEmpty;\n    }\n\n    void IIMEHandler.OnTextChanged(XNATextBox sender) { }\n}\n"
  },
  {
    "path": "ClientGUI/IME/SdlIMEHandler.cs",
    "content": "﻿#nullable enable\nusing Microsoft.Xna.Framework;\n\nnamespace ClientGUI.IME;\n\n/// <summary>\n/// Integrate IME to DesktopGL(SDL2) platform.\n/// </summary>\n/// <remarks>\n/// Note: We were unable to provide reliable input method support for\n/// SDL2 due to the lack of a way to be able to stabilize hooks for\n/// the SDL2 main loop.<br/>\n/// Perhaps this requires some changes in Monogame.\n/// </remarks>\ninternal sealed class SdlIMEHandler(Game game) : DummyIMEHandler\n{\n}"
  },
  {
    "path": "ClientGUI/IME/WinFormsIMEHandler.cs",
    "content": "﻿#nullable enable\nusing System;\n\nusing ImeSharp;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.Tools;\n\nnamespace ClientGUI.IME;\n\n/// <summary>\n/// Integrate IME to XNA framework.\n/// </summary>\ninternal class WinFormsIMEHandler : IMEHandler\n{\n    public override bool TextCompositionEnabled\n    {\n        get => InputMethod.Enabled;\n        protected set\n        {\n            if (value != InputMethod.Enabled)\n                InputMethod.Enabled = value;\n        }\n    }\n\n    public WinFormsIMEHandler(Game game)\n    {\n        Logger.Log($\"Initialize WinFormsIMEHandler.\");\n        if (game?.Window?.Handle == null)\n            throw new Exception(\"The handle of game window should not be null\");\n\n        InputMethod.Initialize(game.Window.Handle);\n        InputMethod.TextInputCallback = OnIMETextInput;\n        InputMethod.TextCompositionCallback = (compositionText, cursorPosition) =>\n        {\n            Composition = compositionText.ToString();\n            CompositionCursorPosition = cursorPosition;\n        };\n    }\n\n    public override void StartTextComposition()\n    {\n        //Debug.WriteLine(\"IME: StartTextComposition\");\n        TextCompositionEnabled = true;\n    }\n\n    public override void StopTextComposition()\n    {\n        //Debug.WriteLine(\"IME: StopTextComposition\");\n        TextCompositionEnabled = false;\n    }\n\n    public override void SetTextInputRectangle(Rectangle rect)\n        => InputMethod.SetTextInputRect(rect.X, rect.Y, rect.Width, rect.Height);\n}\n"
  },
  {
    "path": "ClientGUI/INIConfigException.cs",
    "content": "﻿using System;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// The exception that is thrown when INI data is invalid.\n    /// </summary>\n    public class INIConfigException : Exception\n    {\n        public INIConfigException(string message) : base(message)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/INItializableWindow.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.I18N;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Input;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nnamespace ClientGUI\n{\n    public class INItializableWindow : XNAPanel\n    {\n        public INItializableWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        protected CCIniFile ConfigIni { get; private set; }\n\n        private bool hasCloseButton = false;\n        private bool _initialized = false;\n\n        /// <summary>\n        /// If not null, the client will read an INI file with this name\n        /// instead of the window's name.\n        /// </summary>\n        protected string IniNameOverride { get; set; }\n\n        private static bool AnyChildMatches(IEnumerable<XNAControl> list, Func<XNAControl, bool> isTargetControl)\n        {\n            foreach (XNAControl child in list)\n            {\n                bool matched = isTargetControl(child);\n\n                if (matched)\n                    return true;\n\n                matched = AnyChildMatches(child.Children, isTargetControl);\n\n                if (matched)\n                    return true;\n            }\n\n            return false;\n        }\n\n        public T FindChild<T>(string childName, bool optional = false) where T : XNAControl\n        {\n            XNAControl result = null;\n\n            AnyChildMatches(new List<XNAControl>() { this }, control =>\n            {\n                if (control.Name != childName)\n                    return false;\n\n                result = control;\n                return true;\n            });\n\n            if (result == null && !optional)\n                throw new KeyNotFoundException(\"Could not find required child control: \" + childName);\n\n            return (T)result;\n        }\n\n        public List<T> FindChildrenStartWith<T>(string prefix) where T : XNAControl\n        {\n            List<T> result = new List<T>();\n\n            AnyChildMatches(new List<XNAControl>() { this }, control =>\n            {\n                if (string.IsNullOrEmpty(prefix) ||\n                    !string.IsNullOrEmpty(control.Name) && control.Name.StartsWith(prefix))\n                    result.Add((T)control);\n\n                return false;\n            });\n\n            return result;\n        }\n\n        /// <summary>\n        /// Attempts to locate the ini config file for the current control.\n        /// Only return a config path if it exists.\n        /// </summary>\n        /// <returns>The ini config file path</returns>\n        protected string GetConfigPath()\n        {\n            string iniFileName = string.IsNullOrWhiteSpace(IniNameOverride) ? Name : IniNameOverride;\n\n            // get theme specific path\n            FileInfo configIniPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($\"{iniFileName}.ini\"));\n            if (configIniPath.Exists)\n                return configIniPath.FullName;\n\n            // get base path\n            configIniPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($\"{iniFileName}.ini\"));\n            if (configIniPath.Exists)\n                return configIniPath.FullName;\n\n            if (iniFileName == Name)\n                return null; // IniNameOverride must be null, no need to continue\n\n            iniFileName = Name;\n\n            // get theme specific path\n            configIniPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($\"{iniFileName}.ini\"));\n            if (configIniPath.Exists)\n                return configIniPath.FullName;\n\n            // get base path\n            configIniPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($\"{iniFileName}.ini\"));\n            return configIniPath.Exists ? configIniPath.FullName : null;\n        }\n\n        public override void Initialize()\n        {\n            if (_initialized)\n                throw new InvalidOperationException(\"INItializableWindow cannot be initialized twice.\");\n\n            string configIniPath = GetConfigPath();\n\n            if (string.IsNullOrEmpty(configIniPath))\n            {\n                base.Initialize();\n                return;\n            }\n\n            ConfigIni = new CCIniFile(configIniPath);\n\n            if (Parser.Instance == null)\n                _ = new Parser(WindowManager); // Note: Parser.Instance will be set by calling new Parser()\n\n            Parser.Instance.SetPrimaryControl(this);\n            ReadINIForControl(this);\n            ReadLateAttributesForControl(this);\n\n            ParseExtraControls();\n\n            base.Initialize();\n\n            _initialized = true;\n        }\n\n        private void ParseExtraControls()\n        {\n            var section = ConfigIni.GetSection(\"$ExtraControls\");\n\n            if (section == null)\n                return;\n\n            foreach (var kvp in section.Keys)\n            {\n                if (!kvp.Key.StartsWith(\"$CC\"))\n                    continue;\n\n                string[] parts = kvp.Value.Split(':');\n                if (parts.Length != 2)\n                    throw new ClientConfigurationException(\"Invalid $ExtraControl specified in \" + Name + \": \" + kvp.Value);\n\n                if (!Children.Any(child => child.Name == parts[0]))\n                {\n                    var control = CreateChildControl(this, kvp.Value);\n                    control.Name = parts[0];\n                    control.DrawOrder = -Children.Count;\n                    ReadINIForControl(control);\n                }\n            }\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"HasCloseButton\")\n                hasCloseButton = iniFile.GetBooleanValue(Name, key, hasCloseButton);\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        protected void ReadINIForControl(XNAControl control)\n        {\n            var section = ConfigIni.GetSection(control.Name);\n            if (section == null)\n                return;\n\n            Parser.Instance.SetPrimaryControl(this);\n\n            // shorthand for localization function\n            static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true)\n                => Translation.Instance.LookUp(control, attributeName, defaultValue, notify);\n\n            foreach (var kvp in section.Keys)\n            {\n                if (kvp.Key.StartsWith(\"$CC\"))\n                {\n                    var child = CreateChildControl(control, kvp.Value);\n                    ReadINIForControl(child);\n                    child.Initialize();\n\n                    if (child is ICompositeControl composite)\n                    {\n                        foreach (var sc in composite.SubControls)\n                        {\n                            ReadINIForControl(sc);\n                            sc.Initialize();\n                        }\n                    }\n                }\n                else if (kvp.Key == \"$X\")\n                {\n                    control.X = Parser.Instance.GetExprValue(\n                        Localize(control, kvp.Key, kvp.Value, notify: false), control);\n                }\n                else if (kvp.Key == \"$Y\")\n                {\n                    control.Y = Parser.Instance.GetExprValue(\n                        Localize(control, kvp.Key, kvp.Value, notify: false), control);\n                }\n                else if (kvp.Key == \"$Width\")\n                {\n                    control.Width = Parser.Instance.GetExprValue(\n                        Localize(control, kvp.Key, kvp.Value, notify: false), control);\n                }\n                else if (kvp.Key == \"$Height\")\n                {\n                    control.Height = Parser.Instance.GetExprValue(\n                        Localize(control, kvp.Key, kvp.Value, notify: false), control);\n                }\n                else if (kvp.Key == \"$TextAnchor\" && control is XNALabel)\n                {\n                    // TODO refactor these to be more object-oriented\n                    ((XNALabel)control).TextAnchor = (LabelTextAnchorInfo)Enum.Parse(typeof(LabelTextAnchorInfo), kvp.Value);\n                }\n                else if (kvp.Key == \"$AnchorPoint\" && control is XNALabel)\n                {\n                    string[] parts = kvp.Value.Split(',');\n                    if (parts.Length != 2)\n                        throw new FormatException(\"Invalid format for AnchorPoint: \" + kvp.Value);\n                    ((XNALabel)control).AnchorPoint = new Vector2(Parser.Instance.GetExprValue(parts[0], control), Parser.Instance.GetExprValue(parts[1], control));\n                }\n                else if (kvp.Key == \"$LeftClickAction\")\n                {\n                    if (kvp.Value == \"Disable\")\n                        control.LeftClick += (s, e) => Disable();\n                }\n                else\n                {\n                    control.ParseINIAttribute(ConfigIni, kvp.Key, kvp.Value);\n                }\n            }\n        }\n\n        /// <summary>\n        /// Reads a second set of attributes for a control's child controls.\n        /// Enables linking controls to controls that are defined after them.\n        /// </summary>\n        private void ReadLateAttributesForControl(XNAControl control)\n        {\n            var section = ConfigIni.GetSection(control.Name);\n            if (section == null)\n                return;\n\n            var children = Children.ToList();\n            foreach (var child in children)\n            {\n                // This logic should also be enabled for other types in the future,\n                // but it requires changes in XNAUI\n                if (!(child is XNATextBox))\n                    continue;\n\n                var childSection = ConfigIni.GetSection(child.Name);\n                if (childSection == null)\n                    continue;\n\n                string nextControl = childSection.GetStringValue(\"NextControl\", null);\n                if (!string.IsNullOrWhiteSpace(nextControl))\n                {\n                    var otherChild = children.Find(c => c.Name == nextControl);\n                    if (otherChild != null)\n                        ((XNATextBox)child).NextControl = otherChild;\n                }\n\n                string previousControl = childSection.GetStringValue(\"PreviousControl\", null);\n                if (!string.IsNullOrWhiteSpace(previousControl))\n                {\n                    var otherChild = children.Find(c => c.Name == previousControl);\n                    if (otherChild != null)\n                        ((XNATextBox)child).PreviousControl = otherChild;\n                }\n            }\n        }\n\n        private XNAControl CreateChildControl(XNAControl parent, string keyValue)\n        {\n            string[] parts = keyValue.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);\n\n            if (parts.Length != 2)\n                throw new INIConfigException(\"Invalid child control definition \" + keyValue);\n\n            string childName = parts[0];\n            if (string.IsNullOrEmpty(childName))\n                throw new INIConfigException(\"Empty name in child control definition for \" + parent.Name);\n\n            XNAControl childControl = ClientGUICreator.GetXnaControl(parts[1]);\n\n            if (Array.Exists(childName.ToCharArray(), c => !char.IsLetterOrDigit(c) && c != '_'))\n                throw new INIConfigException(\"Names of INItializableWindow child controls must consist of letters, digits and underscores only. Offending name: \" + parts[0]);\n\n            childControl.Name = childName;\n            parent.AddChildWithoutInitialize(childControl);\n            return childControl;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/IToolTipContainer.cs",
    "content": "﻿namespace ClientGUI;\n\npublic interface IToolTipContainer\n{\n    public ToolTip ToolTip { get; }\n    public string ToolTipText { get; set; }\n}"
  },
  {
    "path": "ClientGUI/Parser.cs",
    "content": "﻿/*********************************************************************\n* Dawn of the Tiberium Age MonoGame/XNA CnCNet Client\n* Expression Parser\n* Copyright (C) Rampastring 2022\n* \n* The CnCNet Client is free software: you can redistribute it and/or modify\n* it under the terms of the GNU General Public License as published by\n* the Free Software Foundation, either version 3 of the License, or\n* (at your option) any later version.\n* \n* The CnCNet Client is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU General Public License for more details.\n* \n* You should have received a copy of the GNU General Public License\n* along with this program.If not, see<https://www.gnu.org/licenses/>.\n* \n*********************************************************************/\n\nusing ClientCore;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// Parses arithmetic expressions.\n    /// </summary>\n    class Parser\n    {\n        private const int CHAR_VALUE_ZERO = 48;\n\n        public Parser(WindowManager windowManager)\n        {\n            if (_instance != null)\n                throw new InvalidOperationException(\"Only one instance of Parser can exist at a time.\");\n\n            globalConstants = new Dictionary<string, int>();\n            globalConstants.Add(\"RESOLUTION_WIDTH\", windowManager.RenderResolutionX);\n            globalConstants.Add(\"RESOLUTION_HEIGHT\", windowManager.RenderResolutionY);\n\n            IniSection parserConstantsSection = ClientConfiguration.Instance.GetParserConstants();\n            if (parserConstantsSection != null)\n            {\n                foreach (var kvp in parserConstantsSection.Keys)\n                    globalConstants.Add(kvp.Key, Conversions.IntFromString(kvp.Value, 0));\n            }\n\n            _instance = this;\n        }\n\n        private static Parser _instance;\n        public static Parser Instance => _instance;\n\n        private static Dictionary<string, int> globalConstants;\n\n        public string Input { get; private set; }\n\n        private int tokenPlace;\n        private XNAControl primaryControl;\n        private XNAControl parsingControl;\n\n        private XNAControl GetControl(string controlName)\n        {\n            if (controlName == primaryControl.Name)\n                return primaryControl;\n\n            var control = Find(primaryControl.Children, controlName);\n            if (control == null)\n                throw new KeyNotFoundException($\"Control '{controlName}' not found while parsing input '{Input}'\");\n\n            return control;\n        }\n\n        private XNAControl Find(IEnumerable<XNAControl> list, string controlName)\n        {\n            foreach (XNAControl child in list)\n            {\n                if (child.Name == controlName)\n                    return child;\n\n                XNAControl childOfChild = Find(child.Children, controlName);\n                if (childOfChild != null)\n                    return childOfChild;\n            }\n\n            return null;\n        }\n\n        private int GetConstant(string constantName)\n        {\n            if (!globalConstants.TryGetValue(constantName, out int value))\n                throw new KeyNotFoundException($\"Constant '{constantName}' not found. \" +\n                    $\"Please check [ParserConstants] section in either {ClientConfiguration.CLIENT_SETTINGS} file, \" +\n                    $\"or any possible files that {ClientConfiguration.CLIENT_SETTINGS} depends on, e.g., GlobalThemeSettings.ini.'\");\n\n            return value;\n        }\n\n        public void SetPrimaryControl(XNAControl primaryControl)\n        {\n            this.primaryControl = primaryControl;\n        }\n\n        public int GetExprValue(string input, XNAControl parsingControl)\n        {\n            this.parsingControl = parsingControl;\n            Input = input;\n            tokenPlace = 0;\n            return GetExprValue();\n        }\n\n        private int GetExprValue()\n        {\n            int value = 0;\n\n            while (true)\n            {\n                SkipWhitespace();\n\n                if (IsEndOfInput())\n                    return value;\n\n                char c = Input[tokenPlace];\n\n                if (char.IsDigit(c))\n                {\n                    value = GetInt();\n                }\n                else if (c == '+')\n                {\n                    tokenPlace++;\n                    value += GetNumericalValue();\n                }\n                else if (c == '-')\n                {\n                    tokenPlace++;\n                    value -= GetNumericalValue();\n                }\n                else if (c == '/')\n                {\n                    tokenPlace++;\n                    value /= GetExprValue();\n                }\n                else if (c == '*')\n                {\n                    tokenPlace++;\n                    value *= GetExprValue();\n                }\n                else if (c == '(')\n                {\n                    tokenPlace++;\n                    value = GetExprValue();\n                }\n                else if (c == ')')\n                {\n                    tokenPlace++;\n                    return value;\n                }\n                else if (char.IsUpper(c))\n                {\n                    value = GetConstantValue();\n                }\n                else if (char.IsLower(c))\n                {\n                    value = GetFunctionValue();\n                }\n            }\n        }\n\n        private int GetNumericalValue()\n        {\n            SkipWhitespace();\n\n            if (IsEndOfInput())\n                return 0;\n\n            char c = Input[tokenPlace];\n\n            if (char.IsDigit(c))\n            {\n                return GetInt();\n            }\n            else if (char.IsUpper(c))\n            {\n                return GetConstantValue();\n            }\n            else if (char.IsLower(c))\n            {\n                return GetFunctionValue();\n            }\n            else if (c == '(')\n            {\n                tokenPlace++;\n                return GetExprValue();\n            }\n            else\n            {\n                throw new INIConfigException(\"Unexpected character \" + c + \" when parsing input: \" + Input);\n            }\n        }\n\n        private void SkipWhitespace()\n        {\n            while (true)\n            {\n                if (IsEndOfInput())\n                    return;\n\n                char c = Input[tokenPlace];\n                if (c == ' ' || c == '\\r' || c == '\\n')\n                    tokenPlace++;\n                else\n                    break;\n            }\n        }\n\n        private string GetIdentifier()\n        {\n            string identifierName = \"\";\n\n            while (true)\n            {\n                if (IsEndOfInput())\n                    break;\n\n                char c = Input[tokenPlace];\n                if (char.IsWhiteSpace(c))\n                    break;\n\n                if (!char.IsLetterOrDigit(c) && c != '_' && c != '$' && c != '.')\n                    break;\n\n                identifierName += c.ToString();\n                tokenPlace++;\n            }\n\n            return identifierName;\n        }\n\n        private int GetConstantValue()\n        {\n            string constantName = GetIdentifier();\n            return GetConstant(constantName);\n        }\n\n        private int GetFunctionValue()\n        {\n            string functionName = GetIdentifier();\n            SkipWhitespace();\n            ConsumeChar('(');\n            string paramName = GetIdentifier();\n            SkipWhitespace();\n            ConsumeChar(')');\n\n            if (paramName == \"$ParentControl\")\n            {\n                if (parsingControl.Parent == null)\n                    throw new INIConfigException(\"$ParentControl used for control that has no parent: \" + parsingControl.Name);\n\n                paramName = parsingControl.Parent.Name;\n            }\n            else if (paramName == \"$Self\")\n            {\n                paramName = parsingControl.Name;\n            }\n\n            switch (functionName)\n            {\n                case \"getX\":\n                    return GetControl(paramName).X;\n                case \"getY\":\n                    return GetControl(paramName).Y;\n                case \"getWidth\":\n                    return GetControl(paramName).Width;\n                case \"getHeight\":\n                    return GetControl(paramName).Height;\n                case \"getBottom\":\n                    return GetControl(paramName).Bottom;\n                case \"getRight\":\n                    return GetControl(paramName).Right;\n                case \"horizontalCenterOnParent\":\n                    parsingControl.CenterOnParentHorizontally();\n                    return parsingControl.X;\n                default:\n                    throw new INIConfigException(\"Unknown function \" + functionName + \" in expression \" + Input);\n            }\n        }\n\n        private void ConsumeChar(char token)\n        {\n            if (Input[tokenPlace] != token)\n                throw new INIConfigException($\"Parse error: expected '{token}' in expression {Input}. Instead encountered '{Input[tokenPlace]}'.\");\n\n            tokenPlace++;\n        }\n\n        private int GetInt()\n        {\n            int value = 0;\n            while (true)\n            {\n                if (IsEndOfInput())\n                    return value;\n\n                char c = Input[tokenPlace];\n                if (!char.IsDigit(c))\n                    return value;\n\n                value = (value * 10) + Input[tokenPlace] - CHAR_VALUE_ZERO;\n                tokenPlace++;\n            }\n        }\n\n        private bool IsEndOfInput() => tokenPlace >= Input.Length;\n    }\n}\n"
  },
  {
    "path": "ClientGUI/ScreenResolution.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A single screen resolution.\n    /// </summary>\n    public sealed record ScreenResolution : IComparable<ScreenResolution>\n    {\n\n        /// <summary>\n        /// The width of the resolution in pixels.\n        /// </summary>\n        public int Width { get; }\n\n        /// <summary>\n        /// The height of the resolution in pixels.\n        /// </summary>\n        public int Height { get; }\n\n        public ScreenResolution(int width, int height)\n        {\n            Width = width;\n            Height = height;\n        }\n\n        public ScreenResolution(Rectangle rectangle)\n        {\n            Width = rectangle.Width;\n            Height = rectangle.Height;\n        }\n\n        public ScreenResolution(string resolution)\n        {\n            List<int> resolutionList = resolution.Trim().Split('x').Take(2).Select(int.Parse).ToList();\n            Width = resolutionList[0];\n            Height = resolutionList[1];\n        }\n\n        public static implicit operator ScreenResolution(string resolution) => new(resolution);\n\n        public sealed override string ToString() => Width + \"x\" + Height;\n\n        public static implicit operator string(ScreenResolution resolution) => resolution.ToString();\n\n        public void Deconstruct(out int width, out int height)\n        {\n            width = this.Width;\n            height = this.Height;\n        }\n\n        public static implicit operator ScreenResolution((int Width, int Height) resolutionTuple) => new(resolutionTuple.Width, resolutionTuple.Height);\n\n        public static implicit operator (int Width, int Height)(ScreenResolution resolution) => new(resolution.Width, resolution.Height);\n\n        public bool Fits(ScreenResolution child) => this.Width >= child.Width && this.Height >= child.Height;\n\n        public int CompareTo(ScreenResolution? other)\n        {\n            if (other is null)\n                return 1;\n            return (this.Width, this.Height).CompareTo((other.Width, other.Height));\n        }\n\n        // Accessing GraphicsAdapter.DefaultAdapter requiring DXMainClient.GameClass has been constructed. Lazy loading prevents possible null reference issues for now.\n        private static ScreenResolution? _desktopResolution = null;\n\n        /// <summary>\n        /// The resolution of primary monitor.\n        /// </summary>\n        public static ScreenResolution DesktopResolution =>\n            _desktopResolution ??= new(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height);\n\n        // The default graphic profile supports resolution up to 4096x4096. The number gets even smaller in practice. Therefore, we select 3840 as the limit.\n        public static ScreenResolution HiDefLimitResolution { get; } = \"3840x3840\";\n\n        private static ScreenResolution? _safeMaximumResolution = null;\n\n        /// <summary>\n        /// The resolution of primary monitor, or the maximum resolution supported by the graphic profile, whichever is smaller.\n        /// </summary>\n        public static ScreenResolution SafeMaximumResolution\n        {\n            get\n            {\n#if XNA\n                return _safeMaximumResolution ??= HiDefLimitResolution.Fits(DesktopResolution) ? DesktopResolution : HiDefLimitResolution;\n#else\n                return _safeMaximumResolution ??= DesktopResolution;\n#endif\n            }\n        }\n\n        private static ScreenResolution? _safeFullScreenResolution = null;\n\n        /// <summary>\n        /// The maximum resolution supported by the graphic profile, or the largest full screen resolution supported by the primary monitor, whichever is smaller.\n        /// </summary>\n        public static ScreenResolution SafeFullScreenResolution => _safeFullScreenResolution ??= GetFullScreenResolutions(minWidth: 800, minHeight: 600).Max ?? SafeMaximumResolution;\n\n        public static SortedSet<ScreenResolution> GetFullScreenResolutions(int minWidth, int minHeight) =>\n            GetFullScreenResolutions(minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height);\n        public static SortedSet<ScreenResolution> GetFullScreenResolutions(int minWidth, int minHeight, int maxWidth, int maxHeight)\n        {\n            SortedSet<ScreenResolution> screenResolutions = [];\n\n            foreach (DisplayMode dm in GraphicsAdapter.DefaultAdapter.SupportedDisplayModes)\n            {\n                if (dm.Width < minWidth || dm.Height < minHeight || dm.Width > maxWidth || dm.Height > maxHeight)\n                    continue;\n\n                var resolution = new ScreenResolution(dm.Width, dm.Height);\n\n                // SupportedDisplayModes can include the same resolution multiple times\n                // because it takes the refresh rate into consideration.\n                // Which will be filtered out by HashSet\n\n                screenResolutions.Add(resolution);\n            }\n\n            return screenResolutions;\n        }\n\n        public static readonly IReadOnlyList<ScreenResolution> OptimalWindowedResolutions =\n        [\n            \"1024x600\",\n            \"1024x720\",\n            \"1280x600\",\n            \"1280x720\",\n            \"1280x768\",\n            \"1280x800\",\n        ];\n\n        public const int MAX_INT_SCALE = 9;\n\n        public SortedSet<ScreenResolution> GetIntegerScaledResolutions() =>\n            GetIntegerScaledResolutions(SafeMaximumResolution);\n        public SortedSet<ScreenResolution> GetIntegerScaledResolutions(ScreenResolution maxResolution)\n        {\n            SortedSet<ScreenResolution> resolutions = [];\n            for (int i = 1; i <= MAX_INT_SCALE; i++)\n            {\n                ScreenResolution scaledResolution = (this.Width * i, this.Height * i);\n\n                if (maxResolution.Fits(scaledResolution))\n                    resolutions.Add(scaledResolution);\n                else\n                    break;\n            }\n\n            return resolutions;\n        }\n\n        public static SortedSet<ScreenResolution> GetWindowedResolutions(int minWidth, int minHeight) =>\n            GetWindowedResolutions(minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height);\n        public static SortedSet<ScreenResolution> GetWindowedResolutions(IEnumerable<ScreenResolution> optimalResolutions, int minWidth, int minHeight) =>\n            GetWindowedResolutions(OptimalWindowedResolutions, minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height);\n        public static SortedSet<ScreenResolution> GetWindowedResolutions(int minWidth, int minHeight, int maxWidth, int maxHeight) =>\n            GetWindowedResolutions(OptimalWindowedResolutions, minWidth, minHeight, maxWidth, maxHeight);\n        public static SortedSet<ScreenResolution> GetWindowedResolutions(IEnumerable<ScreenResolution> optimalResolutions, int minWidth, int minHeight, int maxWidth, int maxHeight)\n        {\n            ScreenResolution maxResolution = (maxWidth, maxHeight);\n\n            SortedSet<ScreenResolution> windowedResolutions = [];\n\n            foreach (ScreenResolution optimalResolution in optimalResolutions)\n            {\n                if (optimalResolution.Width < minWidth || optimalResolution.Height < minHeight)\n                    continue;\n\n                if (!maxResolution.Fits(optimalResolution))\n                    continue;\n\n                windowedResolutions.Add(optimalResolution);\n            }\n\n            return windowedResolutions;\n        }\n\n        public static SortedSet<ScreenResolution> GetRecommendedResolutions()\n        {\n            List<ScreenResolution> recommendedResolutions = ClientConfiguration.Instance.RecommendedResolutions.Select(resolution => (ScreenResolution)resolution).ToList();\n            SortedSet<ScreenResolution> scaledRecommendedResolutions = [.. recommendedResolutions.SelectMany(resolution => resolution.GetIntegerScaledResolutions())];\n            return scaledRecommendedResolutions;\n        }\n\n        public static ScreenResolution GetBestRecommendedResolution() =>\n            GetRecommendedResolutions().Max ?? SafeFullScreenResolution;\n\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/FileSettingCheckBox.cs",
    "content": "using ClientCore;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nnamespace ClientGUI.Settings\n{\n    /// <summary>\n    /// A check-box that toggles between two sets of files and saves the setting to user settings file.\n    /// </summary>\n    public class FileSettingCheckBox : SettingCheckBoxBase, IFileSetting\n    {\n        public FileSettingCheckBox(WindowManager windowManager) : base(windowManager) { }\n\n        public FileSettingCheckBox(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey,\n            bool checkAvailability = false, bool resetUnavailableValue = false, bool restartRequired = false)\n            : base(windowManager, defaultValue, settingSection, settingKey, restartRequired)\n        {\n            CheckAvailability = checkAvailability;\n            ResetUnavailableValue = resetUnavailableValue;\n        }\n\n        public bool CheckAvailability { get; set; }\n        public bool ResetUnavailableValue { get; set; }\n\n        private List<FileSourceDestinationInfo> enabledFiles = new List<FileSourceDestinationInfo>();\n        private List<FileSourceDestinationInfo> disabledFiles = new List<FileSourceDestinationInfo>();\n\n        private bool EnabledFilesComplete => enabledFiles.All(f => File.Exists(f.SourcePath));\n        private bool DisabledFilesComplete => disabledFiles.All(f => File.Exists(f.SourcePath));\n\n        // Backwards compatibility with old FileSettingCheckBox implementation.\n        private bool useLegacyImplementation = false;\n        private bool reversed = false;\n\n        public override void GetAttributes(IniFile iniFile)\n        {\n            base.GetAttributes(iniFile);\n\n            var section = iniFile.GetSection(Name);\n\n            if (section == null)\n                return;\n\n            var files = FileSourceDestinationInfo.ParseFSDInfoList(section, \"File\");\n\n            if (files.Count > 0)\n            {\n                enabledFiles = files;\n                useLegacyImplementation = true;\n            }\n            else\n            {\n                enabledFiles = FileSourceDestinationInfo.ParseFSDInfoList(section, \"EnabledFile\");\n                disabledFiles = FileSourceDestinationInfo.ParseFSDInfoList(section, \"DisabledFile\");\n            }\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"CheckAvailability\":\n                    CheckAvailability = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"ResetUnavailableValue\":\n                    ResetUnavailableValue = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"Reversed\":\n                    reversed = Conversions.BooleanFromString(value, false);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public bool RefreshSetting()\n        {\n            if (useLegacyImplementation)\n                return false;\n\n            bool currentValue = Checked;\n\n            if (CheckAvailability)\n            {\n                Enabled = true;\n\n                if (ResetUnavailableValue)\n                {\n                    if (DisabledFilesComplete != EnabledFilesComplete)\n                        Checked = EnabledFilesComplete;\n                    else if (!DisabledFilesComplete && !EnabledFilesComplete)\n                        Checked = DefaultValue;\n                }\n            }\n\n            return Checked != currentValue;\n        }\n\n        public void AddEnabledFile(string source, string destination, FileOperationOption option)\n            => enabledFiles.Add(new FileSourceDestinationInfo(source, destination, option));\n\n        public void AddDisabledFile(string source, string destination, FileOperationOption option)\n            => disabledFiles.Add(new FileSourceDestinationInfo(source, destination, option));\n\n        public override void Load()\n        {\n            if (useLegacyImplementation)\n                Checked = reversed != File.Exists(enabledFiles[0].DestinationPath);\n            else\n                Checked = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue);\n\n            originalState = Checked;\n        }\n\n        public override bool Save()\n        {\n            if (useLegacyImplementation)\n            {\n                if (reversed != Checked)\n                    enabledFiles.ForEach(f => f.Apply());\n                else\n                    enabledFiles.ForEach(f => f.Revert());\n\n                return RestartRequired && (Checked != originalState);\n            }\n\n            bool canBeChecked = !CheckAvailability || EnabledFilesComplete;\n            bool canBeUnchecked = !CheckAvailability || DisabledFilesComplete;\n\n            if (Checked && canBeChecked)\n            {\n                disabledFiles.ForEach(f => f.Revert());\n                enabledFiles.ForEach(f => f.Apply());\n            }\n            else if (!Checked && canBeUnchecked)\n            {\n                enabledFiles.ForEach(f => f.Revert());\n                disabledFiles.ForEach(f => f.Apply());\n            }\n            else // selected state is unavailable, don't do anything\n            {\n                Logger.Log($\"{nameof(FileSettingCheckBox)}: \" +\n                    $\"The selected state ({Checked}) is unavailable in {Name}\");\n                return false;\n            }\n\n            UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked);\n            return RestartRequired && (Checked != originalState);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/FileSettingDropDown.cs",
    "content": "using ClientCore;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace ClientGUI.Settings\n{\n    /// <summary>\n    /// A dropdown that switches between multiple sets of files.\n    /// </summary>\n    public class FileSettingDropDown : SettingDropDownBase, IFileSetting\n    {\n        public FileSettingDropDown(WindowManager windowManager) : base(windowManager) { }\n\n        public FileSettingDropDown(WindowManager windowManager, int defaultValue, string settingSection, string settingKey,\n            bool checkAvailability = false, bool resetUnavailableValue = false, bool restartRequired = false)\n            : base(windowManager, defaultValue, settingSection, settingKey, restartRequired)\n        {\n            CheckAvailability = checkAvailability;\n            ResetUnavailableValue = resetUnavailableValue;\n        }\n\n        private readonly List<List<FileSourceDestinationInfo>> itemFilesList = new List<List<FileSourceDestinationInfo>>();\n\n        public bool CheckAvailability { get; private set; }\n        public bool ResetUnavailableValue { get; private set; }\n\n        public override void GetAttributes(IniFile iniFile)\n        {\n            base.GetAttributes(iniFile);\n\n            var section = iniFile.GetSection(Name);\n            if (section == null)\n                return;\n\n            for (int i = 0; i < Items.Count; i++)\n                itemFilesList.Add(FileSourceDestinationInfo.ParseFSDInfoList(section, $\"Item{i}File\"));\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"CheckAvailability\":\n                    CheckAvailability = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"ResetUnavailableValue\":\n                    ResetUnavailableValue = Conversions.BooleanFromString(value, false);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public bool RefreshSetting()\n        {\n            int currentValue = SelectedIndex;\n\n            if (CheckAvailability)\n            {\n                for (int i = 0; i < Items.Count; i++)\n                {\n                    Items[i].Selectable = true;\n                    foreach (var fileInfo in itemFilesList[i])\n                    {\n                        if (!File.Exists(fileInfo.SourcePath))\n                        {\n                            Items[i].Selectable = false;\n                            break;\n                        }\n                    }\n                }\n\n                if (ResetUnavailableValue && !Items[SelectedIndex].Selectable)\n                    SelectedIndex = DefaultValue;\n            }\n\n            return SelectedIndex != currentValue;\n        }\n\n        public void AddFile(int itemIndex, string source, string destination, FileOperationOption option)\n        {\n            if (itemIndex < 0 || itemIndex >= Items.Count)\n                return;\n\n            if (itemFilesList.Count < itemIndex + 1)\n                itemFilesList.Add(new List<FileSourceDestinationInfo>());\n\n            itemFilesList[itemIndex].Add(new FileSourceDestinationInfo(source, destination, option));\n        }\n\n        public override void Load()\n        {\n            SelectedIndex = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue);\n            originalState = SelectedIndex;\n        }\n\n        public override bool Save()\n        {\n            if (Items[SelectedIndex].Selectable)\n            {\n                for (int i = 0; i < itemFilesList.Count; i++)\n                {\n                    if (i != SelectedIndex)\n                        itemFilesList[i].ForEach(f => f.Revert());\n                }\n\n                itemFilesList[SelectedIndex].ForEach(f => f.Apply());\n            }\n            else // selected item is unavailable, don't do anything\n            {\n                Logger.Log($\"{nameof(FileSettingDropDown)}: \" +\n                    $\"The selected item \\\"{Items[SelectedIndex].Text}\\\" ({Items[SelectedIndex].Tag}) is unavailable in {Name}.\");\n                return false;\n            }\n\n            UserINISettings.Instance.SetValue(SettingSection, SettingKey, SelectedIndex);\n            return RestartRequired && (SelectedIndex != originalState);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/FileSourceDestinationInfo.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Extensions;\nusing Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace ClientGUI.Settings\n{\n    sealed class FileSourceDestinationInfo\n    {\n        private readonly string destinationPath;\n        private readonly string sourcePath;\n\n        public string SourcePath => SafePath.CombineFilePath(ProgramConstants.GamePath, sourcePath);\n\n        public string DestinationPath => SafePath.CombineFilePath(ProgramConstants.GamePath, destinationPath);\n        /// <summary>\n        /// A path where the files edited by user are saved if\n        /// <see cref=\"FileOperationOption\"/> is set to <see cref=\"FileOperationOption.KeepChanges\"/>.\n        /// </summary>\n        public string CachedPath => SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, \"SettingsCache\", sourcePath);\n\n        public FileOperationOption FileOperationOption { get; }\n\n        public FileSourceDestinationInfo(string source, string destination, FileOperationOption option)\n        {\n            sourcePath = source;\n            destinationPath = destination;\n            FileOperationOption = option;\n        }\n\n        /// <summary>\n        /// Constructs a new instance of <see cref=\"FileSourceDestinationInfo\"/> from a given string.\n        /// </summary>\n        /// <param name=\"value\">A string to be parsed.</param>\n        public FileSourceDestinationInfo(string value)\n        {\n            string[] parts = value.Split(',');\n            if (parts.Length < 2)\n                throw new ArgumentException($\"{nameof(FileSourceDestinationInfo)}: \" +\n                    $\"Too few parameters specified in parsed value\", nameof(value));\n\n            FileOperationOption option = default(FileOperationOption);\n            if (parts.Length >= 3)\n            {\n                bool success = Enum.TryParse(parts[2], out option);\n                if (!success)\n                    throw new ArgumentException($\"{nameof(FileSourceDestinationInfo)}: \" +\n                    $\"Error parsing FileOperationOption enum\", nameof(value));\n            }\n\n            sourcePath = parts[0];\n            destinationPath = parts[1];\n            FileOperationOption = option;\n        }\n\n        /// <summary>\n        /// A method which parses certain key list values from an INI section\n        /// into a list of <see cref=\"FileSourceDestinationInfo\"/> objects.\n        /// </summary>\n        /// <param name=\"section\">An INI section to parse key values from.</param>\n        /// <param name=\"iniKeyPrefix\">A string to append index to when\n        /// parsing the values from key list.</param>\n        /// <returns>A <see cref=\"List{FileSourceDestinationInfo}\"/> of all correctly defined <see cref=\"FileSourceDestinationInfo\"/>s.</returns>\n        public static List<FileSourceDestinationInfo> ParseFSDInfoList(IniSection section, string iniKeyPrefix)\n        {\n            if (section == null)\n                throw new ArgumentNullException(nameof(section));\n\n            List<FileSourceDestinationInfo> result = new List<FileSourceDestinationInfo>();\n            string fileInfo;\n\n            for (int i = 0;\n                !string.IsNullOrWhiteSpace(\n                    fileInfo = section.GetStringValue($\"{iniKeyPrefix}{i}\", string.Empty));\n                i++)\n            {\n                result.Add(new FileSourceDestinationInfo(fileInfo));\n            }\n\n            return result;\n        }\n\n        /// <summary>\n        /// Performs file operations from <see cref=\"SourcePath\"/> to\n        /// <see cref=\"DestinationPath\"/> according to <see cref=\"FileOperationOption\"/>.\n        /// </summary>\n        public void Apply()\n        {\n            switch (FileOperationOption)\n            {\n                case FileOperationOption.OverwriteOnMismatch:\n                    string sourceHash = Utilities.CalculateSHA1ForFile(SourcePath);\n                    string destinationHash = Utilities.CalculateSHA1ForFile(DestinationPath);\n\n                    if (sourceHash != destinationHash)\n                        File.Copy(SourcePath, DestinationPath, true);\n\n                    break;\n\n                case FileOperationOption.DontOverwrite:\n                    if (!File.Exists(DestinationPath))\n                        File.Copy(SourcePath, DestinationPath, false);\n\n                    break;\n\n                case FileOperationOption.KeepChanges:\n                    if (!File.Exists(DestinationPath))\n                    {\n                        if (File.Exists(CachedPath))\n                            File.Move(CachedPath, DestinationPath);\n                        else\n                            File.Copy(SourcePath, DestinationPath, true);\n                    }\n\n                    break;\n\n                case FileOperationOption.AlwaysOverwrite:\n                    File.Copy(SourcePath, DestinationPath, true);\n                    break;\n\n                case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly:\n                    FileExtensions.CreateHardLinkFromSource(sourcePath, destinationPath, fallback: true);\n                    new FileInfo(DestinationPath).IsReadOnly = true;\n                    new FileInfo(SourcePath).IsReadOnly = true;\n                    break;\n\n                default:\n                    throw new InvalidOperationException($\"{nameof(FileSourceDestinationInfo)}: \" +\n                        $\"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}\");\n            }\n        }\n\n        /// <summary>\n        /// Performs file operations to undo changes made by <see cref=\"Apply\"/>\n        /// to <see cref=\"DestinationPath\"/> according to <see cref=\"FileOperationOption\"/>.\n        /// </summary>\n        public void Revert()\n        {\n            switch (FileOperationOption)\n            {\n                case FileOperationOption.KeepChanges:\n                    if (File.Exists(DestinationPath))\n                    {\n                        if (!File.Exists(Path.GetDirectoryName(CachedPath)))\n                            SafePath.GetDirectory(Path.GetDirectoryName(CachedPath)).Create();\n\n                        File.Move(DestinationPath, CachedPath);\n                    }\n                    break;\n\n                case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly:\n                case FileOperationOption.OverwriteOnMismatch:\n                case FileOperationOption.DontOverwrite:\n                case FileOperationOption.AlwaysOverwrite:\n                    if (File.Exists(DestinationPath))\n                    {\n                        FileInfo destinationFile = new(DestinationPath);\n                        destinationFile.IsReadOnly = false;\n                        destinationFile.Delete();\n                    }\n\n                    if (FileOperationOption == FileOperationOption.AlwaysOverwrite_LinkAsReadOnly)\n                        new FileInfo(SourcePath).IsReadOnly = false;\n\n                    break;\n\n                default:\n                    throw new InvalidOperationException($\"{nameof(FileSourceDestinationInfo)}: \" +\n                        $\"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}\");\n            }\n        }\n    }\n\n    /// <summary>\n    /// Defines the expected behavior of file operations performed with\n    /// <see cref=\"FileSourceDestinationInfo\"/>.\n    /// </summary>\n    public enum FileOperationOption\n    {\n        AlwaysOverwrite = 0,\n        OverwriteOnMismatch,\n        DontOverwrite,\n        KeepChanges,\n        AlwaysOverwrite_LinkAsReadOnly,\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/IFileSetting.cs",
    "content": "﻿namespace ClientGUI.Settings\n{\n    interface IFileSetting : IUserSetting\n    {\n        /// <summary>\n        /// Determines if the setting availability is checked on runtime.\n        /// </summary>\n        bool CheckAvailability { get; }\n\n        /// <summary>\n        /// Determines if the client would adjust the setting value automatically\n        /// if the current value becomes unavailable.\n        /// </summary>\n        bool ResetUnavailableValue { get; }\n\n        /// <summary>\n        /// Refreshes the setting to account for possible\n        /// changes that could affect it's functionality.\n        /// </summary>\n        /// <returns>A bool that determines whether the \n        /// setting's value was changed.</returns>\n        bool RefreshSetting();\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/IUserSetting.cs",
    "content": "﻿namespace ClientGUI.Settings\n{\n    public interface IUserSetting\n    {\n\n        /// <summary>\n        /// INI section name in user settings file this setting's value is stored in.\n        /// </summary>\n        string SettingSection { get; }\n\n        /// <summary>\n        /// INI key name in user settings file this setting's value is stored in.\n        /// </summary>\n        string SettingKey { get; }\n\n        /// <summary>\n        /// Determines if this setting requires the client to be restarted\n        /// in order to be correctly applied.\n        /// </summary>\n        bool RestartRequired { get; }\n\n        /// <summary>\n        /// Determines if the setting should reset to its default value where applicable once game process exits.\n        /// </summary>\n        public bool ResetToDefaultOnGameExit { get; }\n\n        /// <summary>\n        /// Loads the current value for the user setting.\n        /// </summary>\n        void Load();\n\n        /// <summary>\n        /// Applies operations based on current setting state.\n        /// </summary>\n        /// <returns>A bool that determines whether the \n        /// client needs to restart for changes to apply.</returns>\n        bool Save();\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/SettingCheckBox.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI.Settings\n{\n    /// <summary>\n    /// A check-box for toggling options in user settings INI file.\n    /// </summary>\n    public class SettingCheckBox : SettingCheckBoxBase\n    {\n        public SettingCheckBox(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public SettingCheckBox(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey,\n            bool writeSettingValue = false, string enabledValue = \"\", string disabledValue = \"\", bool restartRequired = false)\n            : base(windowManager, defaultValue, settingSection, settingKey, restartRequired)\n        {\n            WriteSettingValue = writeSettingValue;\n            EnabledSettingValue = enabledValue;\n            DisabledSettingValue = disabledValue;\n        }\n\n        private bool _writeSettingValue;\n        /// <summary>\n        /// If set, use separate enabled / disabled values instead of checkbox's checked state when reading & writing setting to the user settings INI.\n        /// </summary>\n        public bool WriteSettingValue\n        {\n            get => _writeSettingValue;\n            set\n            {\n                _writeSettingValue = value;\n                defaultKeySuffix = _writeSettingValue ? \"_Value\" : \"_Checked\";\n            }\n        }\n\n        /// <summary>\n        /// Value to write instead of true when checkbox is enabled.\n        /// </summary>\n        public string EnabledSettingValue { get; set; } = string.Empty;\n\n        /// <summary>\n        /// Value to write instead of false when checkbox is disabled.\n        /// </summary>\n        public string DisabledSettingValue { get; set; } = string.Empty;\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"WriteSettingValue\":\n                    WriteSettingValue = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"EnabledSettingValue\":\n                    EnabledSettingValue = value;\n                    return;\n                case \"DisabledSettingValue\":\n                    DisabledSettingValue = value;\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public override void Load()\n        {\n            string value = UserINISettings.Instance.GetValue(SettingSection, SettingKey, string.Empty);\n\n            if (WriteSettingValue)\n            {\n                if (value == EnabledSettingValue)\n                    Checked = true;\n                else if (value == DisabledSettingValue)\n                    Checked = false;\n                else\n                    Checked = DefaultValue;\n            }\n            else\n                Checked = Conversions.BooleanFromString(value, DefaultValue);\n\n            originalState = Checked;\n        }\n\n        public override bool Save()\n        {\n            if (WriteSettingValue)\n                UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked ? EnabledSettingValue : DisabledSettingValue);\n            else\n                UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked);\n\n            return RestartRequired && (Checked != originalState);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/SettingCheckBoxBase.cs",
    "content": "﻿using System;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI.Settings\n{\n    public abstract class SettingCheckBoxBase : XNAClientCheckBox, IUserSetting\n    {\n        public SettingCheckBoxBase(WindowManager windowManager) : base(windowManager) { }\n\n        public SettingCheckBoxBase(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey, bool restartRequired = false, bool resetPerGameSession = false) : base(windowManager)\n        {\n            DefaultValue = defaultValue;\n            SettingSection = settingSection;\n            SettingKey = settingKey;\n            RestartRequired = restartRequired;\n            ResetToDefaultOnGameExit = resetPerGameSession;\n        }\n\n        public bool DefaultValue { get; set; }\n\n        private string _settingSection;\n        public string SettingSection\n        {\n            get => string.IsNullOrEmpty(_settingSection) ? defaultSection : _settingSection;\n            set => _settingSection = value;\n        }\n\n        private string _settingKey;\n        public string SettingKey\n        {\n            get => string.IsNullOrEmpty(_settingKey) ? $\"{Name}{defaultKeySuffix}\" : _settingKey;\n            set => _settingKey = value;\n        }\n\n        public bool RestartRequired { get; set; }\n        public bool ResetToDefaultOnGameExit { get; set; }\n\n        private string _parentCheckBoxName;\n        /// <summary>\n        /// Name of parent check-box control.\n        /// </summary>\n        public string ParentCheckBoxName\n        {\n            get { return _parentCheckBoxName; }\n            set\n            {\n                _parentCheckBoxName = value;\n                UpdateParentCheckBox(FindParentCheckBox());\n            }\n        }\n\n        private XNAClientCheckBox _parentCheckBox;\n        /// <summary>\n        /// Parent check-box control.\n        /// </summary>\n        public XNAClientCheckBox ParentCheckBox\n        {\n            get { return _parentCheckBox; }\n            set\n            {\n                UpdateParentCheckBox(value);\n                _parentCheckBoxName = _parentCheckBox != null ? _parentCheckBox.Name : null;\n            }\n        }\n\n        /// <summary>\n        /// Value required from parent check-box control if set.\n        /// </summary>\n        public bool ParentCheckBoxRequiredValue { get; set; } = true;\n\n        protected string defaultSection = \"CustomSettings\";\n        protected string defaultKeySuffix = \"_Checked\";\n        protected bool originalState;\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"Checked\":\n                case \"DefaultValue\":\n                    DefaultValue = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"SettingSection\":\n                    SettingSection = string.IsNullOrEmpty(value) ? SettingSection : value;\n                    return;\n                case \"SettingKey\":\n                    SettingKey = string.IsNullOrEmpty(value) ? SettingKey : value;\n                    return;\n                case \"RestartRequired\":\n                    RestartRequired = Conversions.BooleanFromString(value, false);\n                    return;\n                case \"ParentCheckBoxName\":\n                    ParentCheckBoxName = value;\n                    return;\n                case \"ParentCheckBoxRequiredValue\":\n                    ParentCheckBoxRequiredValue = Conversions.BooleanFromString(value, true);\n                    return;\n                case \"ResetToDefaultOnGameExit\":\n                    ResetToDefaultOnGameExit = Conversions.BooleanFromString(value, false);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public abstract void Load();\n\n        public abstract bool Save();\n\n\n        private XNAClientCheckBox FindParentCheckBox()\n        {\n            if (string.IsNullOrEmpty(ParentCheckBoxName))\n                return null;\n\n            foreach (var control in Parent.Children)\n            {\n                if (control is XNAClientCheckBox && control.Name == ParentCheckBoxName)\n                    return control as XNAClientCheckBox;\n            }\n\n            return null;\n        }\n\n        private void UpdateParentCheckBox(XNAClientCheckBox parentCheckBox)\n        {\n            if (ParentCheckBox != null)\n                ParentCheckBox.CheckedChanged -= ParentCheckBox_CheckedChanged;\n\n            _parentCheckBox = parentCheckBox;\n            UpdateAllowChecking();\n\n            if (ParentCheckBox != null)\n                ParentCheckBox.CheckedChanged += ParentCheckBox_CheckedChanged;\n        }\n\n        private void ParentCheckBox_CheckedChanged(object sender, EventArgs e) => UpdateAllowChecking();\n\n        private void UpdateAllowChecking()\n        {\n            if (ParentCheckBox != null)\n            {\n                if (ParentCheckBox.Checked == ParentCheckBoxRequiredValue)\n                {\n                    AllowChecking = true;\n                }\n                else\n                {\n                    AllowChecking = false;\n                    Checked = false;\n                }\n            }\n        }\n\n    }\n}"
  },
  {
    "path": "ClientGUI/Settings/SettingDropDown.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI.Settings\n{\n    /// <summary>\n    /// Dropdown for toggling options in user settings INI file.\n    /// </summary>\n    public class SettingDropDown : SettingDropDownBase\n    {\n        public SettingDropDown(WindowManager windowManager) : base(windowManager) { }\n\n        public SettingDropDown(WindowManager windowManager, int defaultValue, string settingSection, string settingKey, bool writeItemValue = false, bool restartRequired = false)\n            : base(windowManager, defaultValue, settingSection, settingKey, restartRequired)\n        {\n            WriteItemValue = writeItemValue;\n        }\n\n        private bool _writeItemValue;\n        /// <summary>\n        /// If set, dropdown item's value instead of index is written to the user settings INI.\n        /// </summary>\n        public bool WriteItemValue\n        {\n            get => _writeItemValue;\n            set\n            {\n                _writeItemValue = value;\n                defaultKeySuffix = _writeItemValue ? \"_Value\" : \"_SelectedIndex\";\n            }\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"WriteItemValue\":\n                    WriteItemValue = Conversions.BooleanFromString(value, false);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public override void Load()\n        {\n            if (WriteItemValue)\n                SelectedIndex = FindItemIndexByValue(UserINISettings.Instance.GetValue(SettingSection, SettingKey, null));\n            else\n                SelectedIndex = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue);\n\n            originalState = SelectedIndex;\n        }\n\n        public override bool Save()\n        {\n            if (WriteItemValue)\n                UserINISettings.Instance.SetValue(SettingSection, SettingKey, (string)SelectedItem.Tag);\n            else\n                UserINISettings.Instance.SetValue(SettingSection, SettingKey, SelectedIndex);\n\n            return RestartRequired && (SelectedIndex != originalState);\n        }\n\n        private int FindItemIndexByValue(string value)\n        {\n            if (string.IsNullOrEmpty(value))\n                return DefaultValue;\n\n            int index = Items.FindIndex(x => (string)x.Tag == value);\n\n            if (index < 0)\n                return DefaultValue;\n\n            return index;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/Settings/SettingDropDownBase.cs",
    "content": "﻿using ClientCore.I18N;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI.Settings\n{\n    public abstract class SettingDropDownBase : XNAClientDropDown, IUserSetting\n    {\n        public SettingDropDownBase(WindowManager windowManager) : base(windowManager) { }\n\n        public SettingDropDownBase(WindowManager windowManager, int defaultValue, string settingSection, string settingKey, bool restartRequired = false)\n            : base(windowManager)\n        {\n            DefaultValue = defaultValue;\n            SettingSection = settingSection;\n            SettingKey = settingKey;\n            RestartRequired = restartRequired;\n        }\n\n        public int DefaultValue { get; set; }\n\n        private string _settingSection;\n        public string SettingSection\n        {\n            get => string.IsNullOrEmpty(_settingSection) ? defaultSection : _settingSection;\n            set => _settingSection = value;\n        }\n\n        private string _settingKey;\n        public string SettingKey\n        {\n            get => string.IsNullOrEmpty(_settingKey) ? $\"{Name}{defaultKeySuffix}\" : _settingKey;\n            set => _settingKey = value;\n        }\n\n        public bool RestartRequired { get; set; }\n        public bool ResetToDefaultOnGameExit { get; set; }\n\n        protected string defaultSection = \"CustomSettings\";\n        protected string defaultKeySuffix = \"_SelectedIndex\";\n        protected int originalState;\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            // shorthand for localization function\n            static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true)\n                => Translation.Instance.LookUp(control, attributeName, defaultValue, notify);\n\n            switch (key)\n            {\n                case \"Items\":\n                    string[] items = value.Split(',');\n                    for (int i = 0; i < items.Length; i++)\n                    {\n                        XNADropDownItem item = new XNADropDownItem\n                        {\n                            Text = Localize(this, $\"Item{i}\", items[i]),\n                            Tag = items[i]\n                        };\n                        AddItem(item);\n                    }\n                    return;\n                case \"DefaultValue\":\n                    DefaultValue = Conversions.IntFromString(value, 0);\n                    return;\n                case \"SettingSection\":\n                    SettingSection = string.IsNullOrEmpty(value) ? SettingSection : value;\n                    return;\n                case \"SettingKey\":\n                    SettingKey = string.IsNullOrEmpty(value) ? SettingKey : value;\n                    return;\n                case \"RestartRequired\":\n                    RestartRequired = Conversions.BooleanFromString(value, false);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public abstract void Load();\n\n        public abstract bool Save();\n    }\n}"
  },
  {
    "path": "ClientGUI/ToolTip.cs",
    "content": "using ClientCore;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A tool tip.\n    /// </summary>\n    public class ToolTip : XNAControl\n    {\n        /// <summary>\n        /// If set to true - makes tooltip not appear and instantly hides it if currently shown.\n        /// </summary>\n        public bool Blocked { get; set; }\n\n        /// <summary>\n        /// Whether the tooltip should move with the cursor after it was shown.\n        /// </summary>\n        public bool FollowCursor { get; set; }\n\n        /// <summary>\n        /// Creates a new tool tip and attaches it to the given control.\n        /// </summary>\n        /// <param name=\"windowManager\">The window manager.</param>\n        /// <param name=\"masterControl\">The control to attach the tool tip to.</param>\n        public ToolTip(WindowManager windowManager, XNAControl masterControl) : base(windowManager)\n        {\n            this.masterControl = masterControl ?? throw new ArgumentNullException(\"masterControl\");\n            masterControl.MouseEnter += MasterControl_MouseEnter;\n            masterControl.MouseLeave += MasterControl_MouseLeave;\n            masterControl.MouseMove += MasterControl_MouseMove;\n            masterControl.EnabledChanged += MasterControl_EnabledChanged;\n            InputEnabled = false;\n            DrawOrder = int.MaxValue;\n            GetParentControl(masterControl.Parent).AddChild(this);\n            Visible = false;\n        }\n\n        private XNAControl GetParentControl(XNAControl parent)\n        {\n            if (parent is XNAWindow)\n                return parent as XNAWindow;\n            else if (parent is INItializableWindow)\n                return parent as INItializableWindow;\n            else if (parent.Parent != null)\n                return GetParentControl(parent.Parent);\n            else\n                return parent;\n        }\n\n        private void MasterControl_EnabledChanged(object sender, EventArgs e)\n            => Enabled = masterControl.Enabled;\n\n        public override string Text\n        {\n            get => base.Text;\n            set\n            {\n                base.Text = value;\n                Vector2 textSize = Renderer.GetTextDimensions(base.Text ?? string.Empty, ClientConfiguration.Instance.ToolTipFontIndex);\n                Width = (int)textSize.X + ClientConfiguration.Instance.ToolTipMargin * 2;\n                Height = (int)textSize.Y + ClientConfiguration.Instance.ToolTipMargin * 2;\n\n                if (string.IsNullOrEmpty(Text))\n                {\n                    Alpha = 0f;\n                    Visible = false;\n                }\n            }\n        }\n\n        public override float Alpha { get; set; }\n        public bool IsMasterControlOnCursor { get; set; }\n\n        private XNAControl masterControl;\n\n        private TimeSpan cursorTime = TimeSpan.Zero;\n\n        private void MasterControl_MouseEnter(object sender, EventArgs e)\n        {\n            IsMasterControlOnCursor = true;\n\n            if (string.IsNullOrEmpty(Text))\n                return;\n\n            DisplayAtLocation(SumPoints(WindowManager.Cursor.Location,\n                new Point(ClientConfiguration.Instance.ToolTipOffsetX, ClientConfiguration.Instance.ToolTipOffsetY)));\n        }\n\n        private void MasterControl_MouseLeave(object sender, EventArgs e)\n        {\n            IsMasterControlOnCursor = false;\n            cursorTime = TimeSpan.Zero;\n        }\n\n        private void MasterControl_MouseMove(object sender, EventArgs e)\n        {\n            if ((FollowCursor || !Visible) && !string.IsNullOrEmpty(Text))\n            {\n                // Move the tooltip if the cursor has moved while staying \n                // on the control area and we're invisible or we follow the cursor\n                DisplayAtLocation(SumPoints(WindowManager.Cursor.Location,\n                    new Point(ClientConfiguration.Instance.ToolTipOffsetX, ClientConfiguration.Instance.ToolTipOffsetY)));\n            }\n        }\n\n        /// <summary>\n        /// Sets the tool tip's location, checking that it doesn't exceed the window's bounds.\n        /// </summary>\n        /// <param name=\"location\">The point at location coordinates.</param>\n        public void DisplayAtLocation(Point location)\n        {\n            X = location.X + Width > WindowManager.RenderResolutionX ?\n                WindowManager.RenderResolutionX - Width : location.X;\n            Y = location.Y - Height < 0 ? 0 : location.Y - Height;\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (Blocked || string.IsNullOrEmpty(Text))\n            {\n                Alpha = 0f;\n                Visible = false;\n                return;\n            }\n\n            if (IsMasterControlOnCursor)\n            {\n                cursorTime += gameTime.ElapsedGameTime;\n\n                if (cursorTime > TimeSpan.FromSeconds(ClientConfiguration.Instance.ToolTipDelay))\n                {\n                    Alpha += ClientConfiguration.Instance.ToolTipAlphaRatePerSecond * (float)gameTime.ElapsedGameTime.TotalSeconds;\n                    Visible = true;\n                    if (Alpha > 1.0f)\n                        Alpha = 1.0f;\n                    return;\n                }\n            }\n\n            Alpha -= ClientConfiguration.Instance.ToolTipAlphaRatePerSecond * (float)gameTime.ElapsedGameTime.TotalSeconds;\n            if (Alpha < 0f)\n            {\n                Alpha = 0f;\n                Visible = false;\n            }\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            Renderer.FillRectangle(ClientRectangle,\n                UISettings.ActiveSettings.BackgroundColor * Alpha);\n            Renderer.DrawRectangle(ClientRectangle,\n                UISettings.ActiveSettings.AltColor * Alpha);\n            Renderer.DrawString(Text, ClientConfiguration.Instance.ToolTipFontIndex,\n                new Vector2(X + ClientConfiguration.Instance.ToolTipMargin, Y + ClientConfiguration.Instance.ToolTipMargin),\n                UISettings.ActiveSettings.AltColor * Alpha, 1.0f);\n        }\n\n        private Point SumPoints(Point p1, Point p2)\n            // This is also needed for XNA compatibility\n#if XNA\n            => new Point(p1.X + p2.X, p1.Y + p2.Y);\n#else\n            => p1 + p2;\n#endif\n    }\n}\n"
  },
  {
    "path": "ClientGUI/TranslationGUIExtensions.cs",
    "content": "using ClientCore.I18N;\n\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI;\n\npublic static class TranslationGUIExtensions\n{\n    /// <summary>\n    /// Looks up the translated value that corresponds to the given INI-defined control attribute.\n    /// </summary>\n    /// <param name=\"control\">The control to look up the attribute value for.</param>\n    /// <param name=\"attributeName\">The attribute name as written in the INI.</param>\n    /// <param name=\"defaultValue\">The value to fall back to in case there's no translated value.</param>\n    /// <param name=\"notify\">Whether to add this key and value to the list of missing key-values.</param>\n    /// <returns>The translated value or a default value.</returns>\n    public static string LookUp(this Translation @this, XNAControl control, string attributeName, string defaultValue, bool notify = true)\n    {\n        string key = $\"INI:Controls:{control.Parent?.Name ?? \"Global\"}:{control.Name}:{attributeName}\";\n        string globalKey = $\"INI:Controls:Global:{control.Name}:{attributeName}\";\n\n        return @this.LookUp(key, fallbackKey: globalKey, defaultValue, notify);\n    }\n}"
  },
  {
    "path": "ClientGUI/TranslationINIParser.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\nusing ClientCore;\nusing ClientCore.Extensions;\nusing ClientCore.I18N;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI;\n\npublic class TranslationINIParser : IControlINIAttributeParser\n{\n    private static TranslationINIParser _instance;\n    public static TranslationINIParser Instance => _instance ??= new TranslationINIParser();\n\n    // shorthand for localization function\n    private string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true)\n        => Translation.Instance.LookUp(control, attributeName, defaultValue, notify);\n\n    public bool ParseINIAttribute(XNAControl control, IniFile iniFile, string key, string value)\n    {\n        switch (key)\n        {\n            case \"Text\":\n                control.Text = Localize(control, key, value.FromIniString());\n                return true;\n            case \"Size\":\n                string[] size = Localize(control, key, value, notify: false).Split(',');\n                control.ClientRectangle = new Rectangle(control.X, control.Y,\n                    int.Parse(size[0], CultureInfo.InvariantCulture),\n                    int.Parse(size[1], CultureInfo.InvariantCulture));\n                return true;\n            case \"Width\":\n                control.Width = int.Parse(Localize(control, key, value, notify: false),\n                    CultureInfo.InvariantCulture);\n                return true;\n            case \"Height\":\n                control.Height = int.Parse(Localize(control, key, value, notify: false),\n                    CultureInfo.InvariantCulture);\n                return true;\n            case \"Location\":\n                string[] location = Localize(control, key, value, notify: false).Split(',');\n                control.ClientRectangle = new Rectangle(\n                    int.Parse(location[0], CultureInfo.InvariantCulture),\n                    int.Parse(location[1], CultureInfo.InvariantCulture),\n                    control.Width, control.Height);\n                return true;\n            case \"X\":\n                control.X = int.Parse(Localize(control, key, value, notify: false),\n                    CultureInfo.InvariantCulture);\n                return true;\n            case \"Y\":\n                control.Y = int.Parse(Localize(control, key, value, notify: false),\n                    CultureInfo.InvariantCulture);\n                return true;\n            case \"DistanceFromRightBorder\":\n                if (control.Parent != null)\n                {\n                    control.ClientRectangle = new Rectangle(\n                        control.Parent.Width\n                            - control.Width\n                            - Conversions.IntFromString(Localize(control, key, value, notify: false), 0),\n                        control.Y,\n                        control.Width, control.Height);\n                }\n                return true;\n            case \"DistanceFromBottomBorder\":\n                if (control.Parent != null)\n                {\n                    control.ClientRectangle = new Rectangle(\n                        control.X,\n                        control.Parent.Height\n                            - control.Height\n                            - Conversions.IntFromString(Localize(control, key, value, notify: false), 0),\n                        control.Width, control.Height);\n                }\n                return true;\n            case \"ToolTip\" when control is IToolTipContainer controlWithToolTip:\n                controlWithToolTip.ToolTipText = Localize(control, key, value.FromIniString());\n                return true;\n            case \"Suggestion\" when control is XNASuggestionTextBox suggestionTextBox:\n                suggestionTextBox.Suggestion = Localize(control, key, value.FromIniString());\n                return true;\n            case \"URL\" when control is XNALinkButton button:  // need to link localized docs\n                button.URL = Localize(control, key, value.FromIniString(), notify: false);\n                return true;\n            case \"UnixURL\" when control is XNALinkButton button:\n                button.UnixURL = Localize(control, key, value.FromIniString(), notify: false);\n                return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "ClientGUI/UIDesignConstants.cs",
    "content": "﻿namespace ClientGUI\n{\n    /// <summary>\n    /// Contains constants used in user interface design.\n    /// </summary>\n    public static class UIDesignConstants\n    {\n        public const int EMPTY_SPACE_SIDES = 6;\n        public const int EMPTY_SPACE_TOP = 6;\n        public const int EMPTY_SPACE_BOTTOM = 6;\n\n        public const int CONTROL_VERTICAL_MARGIN = 6;\n        public const int CONTROL_HORIZONTAL_MARGIN = 6;\n\n        public const int BUTTON_HEIGHT = 23;\n        public const int BUTTON_WIDTH_75 = 75;\n        public const int BUTTON_WIDTH_92 = 92;\n        public const int BUTTON_WIDTH_121 = 121;\n        public const int BUTTON_WIDTH_133 = 133;\n        public const int BUTTON_WIDTH_160 = 160;\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAChatTextBox.cs",
    "content": "﻿using Microsoft.Xna.Framework.Input;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A text box that stores entered messages and allows viewing them\n    /// with the arrow keys.\n    /// </summary>\n    public class XNAChatTextBox : XNASuggestionTextBox\n    {\n        public XNAChatTextBox(WindowManager windowManager) : base(windowManager)\n        {\n            EnterPressed += XNAChatTextBox_EnterPressed;\n        }\n\n        private LinkedList<string> enteredMessages = new LinkedList<string>();\n        private LinkedListNode<string> currentNode;\n\n        private void XNAChatTextBox_EnterPressed(object sender, EventArgs e)\n        {\n            if (!string.IsNullOrEmpty(Text))\n                enteredMessages.AddFirst(Text);\n        }\n\n        protected override bool HandleKeyPress(Keys key)\n        {\n            if (key == Keys.Up)\n            {\n                if (currentNode == null)\n                {\n                    if (enteredMessages.First != null)\n                        currentNode = enteredMessages.First;\n                }\n                else\n                {\n                    if (currentNode.Next != null)\n                        currentNode = currentNode.Next;\n                }\n\n                if (currentNode != null)\n                    Text = currentNode.Value;\n\n                return true;\n            }\n\n            if (key == Keys.Down)\n            {\n                if (currentNode != null && currentNode.Previous != null)\n                {\n                    currentNode = currentNode.Previous;\n                    Text = currentNode.Value;\n                }\n\n                return true;\n            }\n\n            currentNode = null;\n            return base.HandleKeyPress(key);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientButton.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\nusing Rampastring.Tools;\nusing System;\nusing ClientCore;\nusing ClientCore.Extensions;\n\nnamespace ClientGUI\n{\n    public class XNAClientButton : XNAButton, IToolTipContainer\n    {\n        public ToolTip ToolTip { get; private set; }\n\n        private string _initialToolTipText;\n        public string ToolTipText\n        {\n            get => Initialized ? ToolTip?.Text : _initialToolTipText;\n            set\n            {\n                if (Initialized)\n                    ToolTip.Text = value;\n                else\n                    _initialToolTipText = value;\n            }\n        }\n\n        public XNAClientButton(WindowManager windowManager) : base(windowManager)\n        {\n            FontIndex = 1;\n            Height = UIDesignConstants.BUTTON_HEIGHT;\n        }\n\n        public override void Initialize()\n        {\n            int width = Width;\n\n            if (IdleTexture == null)\n                IdleTexture = AssetLoader.LoadTexture(width + \"pxbtn.png\");\n\n            if (HoverTexture == null)\n                HoverTexture = AssetLoader.LoadTexture(width + \"pxbtn_c.png\");\n\n            if (HoverSoundEffect == null)\n                HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n\n            base.Initialize();\n\n            if (Width == 0)\n                Width = IdleTexture.Width;\n\n            ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText };\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"MatchTextureSize\" && Conversions.BooleanFromString(value, false))\n            {\n                Width = IdleTexture.Width;\n                Height = IdleTexture.Height;\n                return;\n            }\n            else if (key == \"ToolTip\")\n            {\n                ToolTipText = value.FromIniString();\n                return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientCheckBox.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\nusing System;\nusing Rampastring.Tools;\nusing ClientCore;\nusing ClientCore.Extensions;\n\nnamespace ClientGUI\n{\n    public class XNAClientCheckBox : XNACheckBox, IToolTipContainer\n    {\n        public ToolTip ToolTip { get; private set; }\n\n        private string _initialToolTipText;\n        public string ToolTipText\n        {\n            get => Initialized ? ToolTip?.Text : _initialToolTipText;\n            set\n            {\n                if (Initialized)\n                    ToolTip.Text = value;\n                else\n                    _initialToolTipText = value;\n            }\n        }\n\n        public XNAClientCheckBox(WindowManager windowManager) : base(windowManager) { }\n\n        public override void Initialize()\n        {\n            CheckSoundEffect = new EnhancedSoundEffect(\"checkbox.wav\");\n\n            base.Initialize();\n\n            ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText };\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"ToolTip\")\n            {\n                ToolTipText = value.FromIniString();\n                return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientColorDropDown.cs",
    "content": "﻿using System.Collections.Generic;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI\n{\n    public class XNAClientColorDropDown : XNAClientDropDown\n    {\n        private const int VERTICAL_PADDING = 3;\n        private const int HORIZONTAL_PADDING = 2;\n        public ItemsKind ItemsDrawMode { get; private set; } = ItemsKind.TextAndIcon;\n\n        public int ColorTextureWidth { get; private set; }\n        public int ColorTextureHeight { get; private set; }\n        public Texture2D RandomColorTexture { get; private set; }\n        public Texture2D DisabledItemTexture { get; private set; }\n\n        private Dictionary<int, Texture2D> itemColorTextures = new Dictionary<int, Texture2D>();\n\n        public XNAClientColorDropDown(WindowManager windowManager) : base(windowManager)\n        {\n            ColorTextureWidth = Height - VERTICAL_PADDING;\n            ColorTextureHeight = Height - HORIZONTAL_PADDING;\n            RandomColorTexture = AssetLoader.LoadTexture(\"randomicon.png\");\n            DisabledItemTexture = AssetLoader.CreateTexture(DisabledItemColor, ColorTextureWidth, ColorTextureHeight);\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case nameof(ItemsDrawMode):\n                    ItemsDrawMode = value.FromIniString().ToEnum<ItemsKind>();\n\n                    switch (ItemsDrawMode)\n                    {\n                        case ItemsKind.Text:\n                            for (int i = 0; i < Items.Count; i++)\n                            {\n                                // Text mode: use transparent 1x1 texture as placeholder\n                                var texture = AssetLoader.CreateTexture(AssetLoader.GetRGBAColorFromString(\"0,0,0,0\"), 1, 1);\n                                Items[i].Texture = texture;\n                                itemColorTextures[i] = texture;\n                            }\n                            break;\n                        case ItemsKind.Icon:\n                            ColorTextureWidth = Width - VERTICAL_PADDING;\n                            ColorTextureHeight = Height - HORIZONTAL_PADDING;\n\n                            for (int i = 0; i < Items.Count; i++)\n                            {\n                                if (i != 0) // Skip random color item\n                                {\n                                    var texture = AssetLoader.CreateTexture(\n                                        Items[i].TextColor ?? Color.White,\n                                        ColorTextureWidth,\n                                        ColorTextureHeight);\n                                    Items[i].Texture = texture;\n                                    itemColorTextures[i] = texture;\n                                }\n\n                                Items[i].Text = string.Empty;\n                            }\n\n                            DisabledItemTexture = AssetLoader.CreateTexture(DisabledItemColor, Width - VERTICAL_PADDING, Height - HORIZONTAL_PADDING);\n\n                            break;\n                        case ItemsKind.TextAndIcon:\n                            break;\n                        default:\n                            break;\n                    }\n\n                    return;\n                case nameof(ColorTextureWidth):\n                    ColorTextureWidth = Conversions.IntFromString(value, ColorTextureWidth);\n                    break;\n                case nameof(ColorTextureHeight):\n                    ColorTextureHeight = Conversions.IntFromString(value, ColorTextureHeight);\n                    break;\n                case nameof(RandomColorTexture):\n                    RandomColorTexture = AssetLoader.LoadTexture(value);\n                    Items[0].Texture = RandomColorTexture;\n                    break;\n                case nameof(DisabledItemTexture):\n                    DisabledItemTexture = AssetLoader.LoadTexture(value);\n                    break;\n                default:\n                    base.ParseControlINIAttribute(iniFile, key, value);\n                    return;\n            }\n        }\n\n        public new virtual void AddItem(string text, Color color)\n        {\n            var item = new XNADropDownItem();\n\n            item.Text = text;\n            item.TextColor = color;\n\n            int index = Items.Count;\n\n            if (index > 0) // Not the random color item\n            {\n                var texture = AssetLoader.CreateTexture(color, ColorTextureWidth, ColorTextureHeight);\n                item.Texture = texture;\n                itemColorTextures[index] = texture;\n            }\n            else\n            {\n                item.Texture = RandomColorTexture;\n            }\n\n            Items.Add(item);\n        }\n\n        /// <summary>\n        /// Enables or disables the color texture for an item by swapping between the color texture and disabled texture.\n        /// </summary>\n        /// <param name=\"itemIndex\">The index of the item.</param>\n        /// <param name=\"enabled\">If true, sets the color texture. If false, sets the disabled texture.</param>\n        public void SetItemColorEnabled(int itemIndex, bool enabled)\n        {\n            if (itemIndex < 0 || itemIndex >= Items.Count)\n                return;\n\n            // Skip random color\n            if (itemIndex == 0)\n                return;\n\n            // Skip if in text only mode\n            if (ItemsDrawMode == ItemsKind.Text)\n                return;\n\n            if (enabled)\n            {\n                if (itemColorTextures.TryGetValue(itemIndex, out var colorTexture))\n                    Items[itemIndex].Texture = colorTexture;\n            }\n            else\n            {\n                Items[itemIndex].Texture = DisabledItemTexture;\n            }\n        }\n\n        public enum ItemsKind\n        {\n            Text,\n            Icon,\n            TextAndIcon\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientDropDown.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\nusing Rampastring.Tools;\nusing System;\nusing ClientCore;\nusing ClientCore.Extensions;\n\nnamespace ClientGUI\n{\n    public class XNAClientDropDown : XNADropDown, IToolTipContainer\n    {\n        public ToolTip ToolTip { get; private set; }\n\n        private string _initialToolTipText;\n        public string ToolTipText\n        {\n            get => Initialized ? ToolTip?.Text : _initialToolTipText;\n            set\n            {\n                if (Initialized)\n                    ToolTip.Text = value;\n                else\n                    _initialToolTipText = value;\n            }\n        }\n\n        public XNAClientDropDown(WindowManager windowManager) : base(windowManager) { }\n\n        public override void Initialize()\n        {\n            ClickSoundEffect = new EnhancedSoundEffect(\"dropdown.wav\");\n\n            base.Initialize();\n\n            ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText };\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"ToolTip\")\n            {\n                ToolTipText = value.FromIniString();\n                return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public override void OnMouseLeftDown(InputEventArgs inputEventArgs)\n        {\n            // no need to set Handled to true since we're not \"consuming\" the event here, just augmenting\n            base.OnMouseLeftDown(inputEventArgs);\n            UpdateToolTipBlock();\n        }\n\n        protected override void CloseDropDown()\n        {\n            base.CloseDropDown();\n            UpdateToolTipBlock();\n        }\n\n        protected void UpdateToolTipBlock()\n        {\n            if (DropDownState == DropDownState.CLOSED)\n                ToolTip.Blocked = false;\n            else\n                ToolTip.Blocked = true;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientLinkLabel.cs",
    "content": "﻿using Rampastring.XNAUI;\nusing Rampastring.Tools;\nusing ClientCore;\nusing Rampastring.XNAUI.XNAControls;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// Link label with customizable URL and tooltip text as well as hover/click sounds.\n    /// Also uses hover text color by default.\n    /// </summary>\n    public class XNAClientLinkLabel : XNALinkLabel, IToolTipContainer\n    {\n        public EnhancedSoundEffect HoverSoundEffect { get; set; }\n        public EnhancedSoundEffect ClickSoundEffect { get; set; }\n\n        private Color? _hoverColor;\n\n        /// <summary>\n        /// The color of the label when it's hovered on.\n        /// </summary>\n        public new Color HoverColor\n        {\n            get\n            {\n                return _hoverColor ?? UISettings.ActiveSettings.ButtonHoverColor;\n            }\n            set { _hoverColor = value; if (IsActive) RemapColor = value; }\n        }\n\n        public ToolTip ToolTip { get; private set; }\n\n        private string _initialToolTipText;\n        public string ToolTipText\n        {\n            get => Initialized ? ToolTip?.Text : _initialToolTipText;\n            set\n            {\n                if (Initialized)\n                    ToolTip.Text = value;\n                else\n                    _initialToolTipText = value;\n            }\n        }\n\n        public XNAClientLinkLabel(WindowManager windowManager) : base(windowManager) { }\n\n        public string URL { get; set; }\n        public string UnixURL { get; set; }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText };\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"ToolTip\":\n                    ToolTipText = value.FromIniString();\n                    return;\n                case \"URL\":\n                    URL = value;\n                    return;\n                case \"UnixURL\":\n                    UnixURL = value;\n                    return;\n                case \"HoverSoundEffect\":\n                    HoverSoundEffect = new EnhancedSoundEffect(value);\n                    return;\n                case \"ClickSoundEffect\":\n                    ClickSoundEffect = new EnhancedSoundEffect(value);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public override void OnMouseEnter()\n        {\n            base.OnMouseLeave();\n\n            HoverSoundEffect?.Play();\n\n            RemapColor = HoverColor;\n            TextColor = HoverColor;\n        }\n\n        public override void OnMouseLeave()\n        {\n            base.OnMouseLeave();\n\n            RemapColor = IdleColor;\n            TextColor = IdleColor;\n        }\n\n        public override void OnLeftClick(InputEventArgs inputEventArgs)\n        {\n            inputEventArgs.Handled = true;\n            \n            ClickSoundEffect?.Play();\n\n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n\n            if (osVersion == OSVersion.UNIX && !string.IsNullOrEmpty(UnixURL))\n                ProcessLauncher.StartShellProcess(UnixURL);\n            else if (!string.IsNullOrEmpty(URL))\n                ProcessLauncher.StartShellProcess(URL);\n\n            base.OnLeftClick(inputEventArgs);\n        }\n    }\n}"
  },
  {
    "path": "ClientGUI/XNAClientPreferredItemDropDown.cs",
    "content": "﻿using Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System.Collections.Generic;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A drop-down control that has a preferred drop-down item with an optional string label displayed next to its text.\n    /// </summary>\n    public class XNAClientPreferredItemDropDown : XNAClientDropDown\n    {\n        /// <summary>\n        /// String label displayed next to the preferred drop-down item text.\n        /// </summary>\n        public string PreferredItemLabel { get; set; }\n\n        /// <summary>\n        /// Index of the preferred drop-down item.\n        /// </summary>\n        public List<int> PreferredItemIndexes { get; set; } = new List<int>();\n\n        /// <summary>\n        /// Creates a new preferred item drop-down control.\n        /// </summary>\n        /// <param name=\"windowManager\">The WindowManager associated with this control.</param>\n        public XNAClientPreferredItemDropDown(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"PreferredItemLabel\":\n                    PreferredItemLabel = value;\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        /// <summary>\n        /// Draws the drop-down.\n        /// </summary>\n        public override void Draw(GameTime gameTime)\n        {\n            if (PreferredItemIndexes.Count > 0)\n            {\n                PreferredItemIndexes.ForEach(i =>\n                {\n                    XNADropDownItem preferredItem = Items[i];\n                    string preferredItemOriginalText = preferredItem.Text;\n                    preferredItem.Text += \" \" + PreferredItemLabel;\n                });\n\n                base.Draw(gameTime);\n\n                PreferredItemIndexes.ForEach(i =>\n                {\n                    XNADropDownItem preferredItem = Items[i];\n                    preferredItem.Text = preferredItem.Text.Substring(0, preferredItem.Text.Length - PreferredItemLabel.Length - 1);\n                });\n            }\n            else\n            {\n                base.Draw(gameTime);\n            }\n\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientStateButton.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI\n{\n    public class XNAClientStateButton<T> : XNAButton where T : Enum\n    {\n        private T _state { get; set; }\n\n        private Dictionary<T, Texture2D> StateTextures { get; set; }\n\n        private string _toolTipText { get; set; }\n        private ToolTip _toolTip { get; set; }\n\n        public XNAClientStateButton(WindowManager windowManager, Dictionary<T, Texture2D> textures) : base(windowManager)\n        {\n            LeftClick += CycleState;\n            StateTextures = textures;\n        }\n\n        public override void Initialize()\n        {\n            if (StateTextures == null || StateTextures.Count < 2)\n                throw new ArgumentException(\"State button requires at least 2 states\");\n\n            UpdateStateTexture();\n\n            base.Initialize();\n\n            _toolTip = new ToolTip(WindowManager, this);\n            SetToolTipText(_toolTipText);\n            \n            if (Width == 0)\n                Width = IdleTexture.Width;\n        }\n\n        public void SetState(T state)\n        {\n            if(!Enum.IsDefined(typeof(T), state))\n                throw new IndexOutOfRangeException($\"{state} not a valid texture value\");\n\n            _state = state;\n            UpdateStateTexture();\n        }\n\n        public T GetState() => _state;\n\n        private void CycleState(object sender, EventArgs e)\n        {\n            _state = _state.CycleNext();\n            UpdateStateTexture();\n        }\n\n        public void SetToolTipText(string text)\n        {\n            _toolTipText = text ?? string.Empty;\n            if (_toolTip != null)\n                _toolTip.Text = _toolTipText;\n        }\n\n        private void UpdateStateTexture()\n        {\n            IdleTexture = StateTextures[_state];\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientTabControl.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI\n{\n    public class XNAClientTabControl : XNATabControl\n    {\n        public XNAClientTabControl(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public override void Initialize()\n        {\n            if (ClickSound == null)\n            {\n                ClickSound = new EnhancedSoundEffect(\"button.wav\");\n            }\n            \n            base.Initialize();\n        }\n\n        public void AddTab(string text, int width)\n        {\n            string tabAssetName = width + \"pxtab\";\n\n            if (AssetLoader.AssetExists(tabAssetName + \".png\"))\n            {\n                AddTab(text, AssetLoader.LoadTexture(tabAssetName + \".png\"),\n                    AssetLoader.LoadTexture(tabAssetName + \"_c.png\"));\n            }\n            else\n            {\n                AddTab(text, AssetLoader.LoadTexture(width + \"pxbtn.png\"),\n                    AssetLoader.LoadTexture(width + \"pxbtn_c.png\"));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAClientToggleButton.cs",
    "content": "﻿using System;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// This is a combination of a checkbox and a standard button. You must specify\n    /// the Checked and Unchecked Textures to render for each button state.\n    /// </summary>\n    public class XNAClientToggleButton : XNAButton\n    {\n        public Texture2D CheckedTexture { get; set; }\n        public Texture2D UncheckedTexture { get; set; }\n\n        private string _toolTipText { get; set; }\n        private ToolTip ToolTip { get; set; }\n\n        private bool _checked { get; set; }\n\n        public override void Initialize()\n        {\n            if (CheckedTexture == null)\n                throw new ArgumentNullException(nameof(CheckedTexture));\n\n            if (UncheckedTexture == null)\n                throw new ArgumentNullException(nameof(UncheckedTexture));\n\n            UpdateIdleTexture();\n\n            if (HoverSoundEffect == null)\n                HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n\n            base.Initialize();\n\n            ToolTip = new ToolTip(WindowManager, this);\n            SetToolTipText(_toolTipText);\n\n            if (Width == 0)\n                Width = IdleTexture.Width;\n        }\n\n        public bool Checked\n        {\n            get => _checked;\n            set\n            {\n                _checked = value;\n                UpdateIdleTexture();\n            }\n        }\n\n        private void UpdateIdleTexture()\n        {\n            IdleTexture = _checked ? CheckedTexture : UncheckedTexture;\n        }\n\n        public void SetToolTipText(string text)\n        {\n            _toolTipText = text ?? string.Empty;\n            if (ToolTip != null)\n                ToolTip.Text = _toolTipText;\n        }\n\n        public XNAClientToggleButton(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case nameof(CheckedTexture):\n                    CheckedTexture = AssetLoader.LoadTexture(value);\n                    UpdateIdleTexture();\n                    break;\n                case nameof(UncheckedTexture):\n                    UncheckedTexture = AssetLoader.LoadTexture(value);\n                    UpdateIdleTexture();\n                    break;\n                case nameof(ToolTip):\n                    SetToolTipText(value);\n                    break;\n                default:\n                    base.ParseControlINIAttribute(iniFile, key, value);\n                    break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAExtraPanel.cs",
    "content": "﻿using Microsoft.Xna.Framework;\nusing Rampastring.XNAUI.XNAControls;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// An \"extra panel\" for modders that automatically\n    /// changes its size to match the texture size.\n    /// </summary>\n    public class XNAExtraPanel : XNAPanel\n    {\n        public XNAExtraPanel(WindowManager windowManager) : base(windowManager)\n        {\n            InputEnabled = false;\n            DrawBorders = false;\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"BackgroundTexture\")\n            {\n                BackgroundTexture = AssetLoader.LoadTexture(value);\n\n                if (new Point(Width, Height) == Point.Zero)\n                {\n                    ClientRectangle = new Rectangle(X, Y,\n                        BackgroundTexture.Width, BackgroundTexture.Height);\n                }\n\n                return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNALinkButton.cs",
    "content": "﻿using System;\nusing Rampastring.XNAUI;\nusing Rampastring.Tools;\nusing ClientCore;\n\nnamespace ClientGUI\n{\n    public class XNALinkButton : XNAClientButton\n    {\n        public XNALinkButton(WindowManager windowManager) : base(windowManager) { }\n\n        public string URL { get; set; }\n        public string UnixURL { get; set; }\n        public string Arguments { get; set; }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            if (key == \"URL\")\n            {\n                URL = value;\n                return;\n            }\n\n            if (key == \"UnixURL\")\n            {\n                UnixURL = value;\n                return;\n            }\n\n            if (key == \"Arguments\")\n            {\n                Arguments = value;\n                return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public override void OnLeftClick(InputEventArgs inputEventArgs)\n        {\n            inputEventArgs.Handled = true;\n            \n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n\n            if (osVersion == OSVersion.UNIX && !string.IsNullOrEmpty(UnixURL))\n                ProcessLauncher.StartShellProcess(UnixURL, Arguments);\n            else if (!string.IsNullOrEmpty(URL))\n                ProcessLauncher.StartShellProcess(URL, Arguments);\n\n            base.OnLeftClick(inputEventArgs);\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAMessageBox.cs",
    "content": "﻿using ClientCore.Extensions;\nusing System;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\nusing Microsoft.Xna.Framework.Input;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A generic message box with OK or Yes/No or OK/Cancel buttons.\n    /// </summary>\n    public class XNAMessageBox : XNAWindow\n    {\n        /// <summary>\n        /// Creates a new message box.\n        /// </summary>\n        /// <param name=\"windowManager\">The window manager.</param>\n        /// <param name=\"caption\">The caption of the message box.</param>\n        /// <param name=\"description\">The actual message of the message box.</param>\n        /// <param name=\"messageBoxButtons\">Defines which buttons are available in the dialog.</param>\n        public XNAMessageBox(WindowManager windowManager,\n            string caption, string description, XNAMessageBoxButtons messageBoxButtons)\n            : base(windowManager)\n        {\n            this.caption = caption;\n            this.description = description;\n            this.messageBoxButtons = messageBoxButtons;\n        }\n\n        /// <summary>\n        /// The method that is called when the user clicks OK on the message box.\n        /// </summary>\n        public Action<XNAMessageBox> OKClickedAction { get; set; }\n\n        /// <summary>\n        /// The method that is called when the user clicks Yes on the message box.\n        /// </summary>\n        public Action<XNAMessageBox> YesClickedAction { get; set; }\n\n        /// <summary>\n        /// The method that is called when the user clicks No on the message box.\n        /// </summary>\n        public Action<XNAMessageBox> NoClickedAction { get; set; }\n\n        /// <summary>\n        /// The method that is called when the user clicks Cancel on the message box.\n        /// </summary>\n        public Action<XNAMessageBox> CancelClickedAction { get; set; }\n\n\n        private string caption;\n        private string description;\n        private XNAMessageBoxButtons messageBoxButtons;\n\n        public override void Initialize()\n        {\n            Name = \"MessageBox\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"msgboxform.png\");\n\n            XNALabel lblCaption = new XNALabel(WindowManager);\n            lblCaption.Text = caption;\n            lblCaption.ClientRectangle = new Rectangle(12, 9, 0, 0);\n            lblCaption.FontIndex = 1;\n\n            XNAPanel line = new XNAPanel(WindowManager);\n            line.ClientRectangle = new Rectangle(6, 29, 0, 1);\n\n            XNALabel lblDescription = new XNALabel(WindowManager);\n            lblDescription.Text = description;\n            lblDescription.ClientRectangle = new Rectangle(12, 39, 0, 0);\n\n            AddChild(lblCaption);\n            AddChild(line);\n            AddChild(lblDescription);\n\n            Vector2 textDimensions = Renderer.GetTextDimensions(lblDescription.Text, lblDescription.FontIndex);\n            ClientRectangle = new Rectangle(0, 0, (int)textDimensions.X + 24, (int)textDimensions.Y + 81);\n            line.ClientRectangle = new Rectangle(6, 29, Width - 12, 1);\n\n            if (messageBoxButtons == XNAMessageBoxButtons.OK)\n            {\n                AddOKButton();\n            }\n            else if (messageBoxButtons == XNAMessageBoxButtons.YesNo)\n            {\n                AddYesNoButtons();\n            }\n            else // messageBoxButtons == DXMessageBoxButtons.OKCancel\n            {\n                AddOKCancelButtons();\n            }\n\n            base.Initialize();\n\n            WindowManager.CenterControlOnScreen(this);\n        }\n\n        private void AddOKButton()\n        {\n            XNAButton btnOK = new XNAButton(WindowManager);\n            btnOK.FontIndex = 1;\n            btnOK.ClientRectangle = new Rectangle(0, 0, 75, 23);\n            btnOK.IdleTexture = AssetLoader.LoadTexture(\"75pxbtn.png\");\n            btnOK.HoverTexture = AssetLoader.LoadTexture(\"75pxbtn_c.png\");\n            btnOK.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnOK.Name = \"btnOK\";\n            btnOK.Text = \"OK\".L10N(\"Client:ClientGUI:ButtonOK\");\n            btnOK.LeftClick += BtnOK_LeftClick;\n            btnOK.HotKey = Keys.Enter;\n\n            AddChild(btnOK);\n\n            btnOK.CenterOnParent();\n            btnOK.ClientRectangle = new Rectangle(btnOK.X,\n                Height - 28, btnOK.Width, btnOK.Height);\n        }\n\n        private void AddYesNoButtons()\n        {\n            XNAButton btnYes = new XNAButton(WindowManager);\n            btnYes.FontIndex = 1;\n            btnYes.ClientRectangle = new Rectangle(0, 0, 75, 23);\n            btnYes.IdleTexture = AssetLoader.LoadTexture(\"75pxbtn.png\");\n            btnYes.HoverTexture = AssetLoader.LoadTexture(\"75pxbtn_c.png\");\n            btnYes.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnYes.Name = \"btnYes\";\n            btnYes.Text = \"Yes\".L10N(\"Client:ClientGUI:ButtonYes\");\n            btnYes.LeftClick += BtnYes_LeftClick;\n            btnYes.HotKey = Keys.Y;\n\n            AddChild(btnYes);\n\n            btnYes.ClientRectangle = new Rectangle((Width - ((btnYes.Width + 5) * 2)) / 2,\n                Height - 28, btnYes.Width, btnYes.Height);\n\n            XNAButton btnNo = new XNAButton(WindowManager);\n            btnNo.FontIndex = 1;\n            btnNo.ClientRectangle = new Rectangle(0, 0, 75, 23);\n            btnNo.IdleTexture = AssetLoader.LoadTexture(\"75pxbtn.png\");\n            btnNo.HoverTexture = AssetLoader.LoadTexture(\"75pxbtn_c.png\");\n            btnNo.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnNo.Name = \"btnNo\";\n            btnNo.Text = \"No\".L10N(\"Client:ClientGUI:ButtonNo\");\n            btnNo.LeftClick += BtnNo_LeftClick;\n            btnNo.HotKey = Keys.N;\n\n            AddChild(btnNo);\n\n            btnNo.ClientRectangle = new Rectangle(btnYes.X + btnYes.Width + 10,\n                Height - 28, btnNo.Width, btnNo.Height);\n        }\n\n        private void AddOKCancelButtons()\n        {\n            XNAButton btnOK = new XNAButton(WindowManager);\n            btnOK.FontIndex = 1;\n            btnOK.ClientRectangle = new Rectangle(0, 0, 75, 23);\n            btnOK.IdleTexture = AssetLoader.LoadTexture(\"75pxbtn.png\");\n            btnOK.HoverTexture = AssetLoader.LoadTexture(\"75pxbtn_c.png\");\n            btnOK.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnOK.Name = \"btnOK\";\n            btnOK.Text = \"OK\".L10N(\"Client:ClientGUI:ButtonOK\");\n            btnOK.LeftClick += BtnYes_LeftClick;\n            btnOK.HotKey = Keys.Enter;\n\n            AddChild(btnOK);\n\n            btnOK.ClientRectangle = new Rectangle((Width - ((btnOK.Width + 5) * 2)) / 2,\n                Height - 28, btnOK.Width, btnOK.Height);\n\n            XNAButton btnCancel = new XNAButton(WindowManager);\n            btnCancel.FontIndex = 1;\n            btnCancel.ClientRectangle = new Rectangle(0, 0, 75, 23);\n            btnCancel.IdleTexture = AssetLoader.LoadTexture(\"75pxbtn.png\");\n            btnCancel.HoverTexture = AssetLoader.LoadTexture(\"75pxbtn_c.png\");\n            btnCancel.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnCancel.Name = \"btnCancel\";\n            btnCancel.Text = \"Cancel\".L10N(\"Client:ClientGUI:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n            btnCancel.HotKey = Keys.C;\n\n            AddChild(btnCancel);\n\n            btnCancel.ClientRectangle = new Rectangle(btnOK.X + btnOK.Width + 10,\n                Height - 28, btnCancel.Width, btnCancel.Height);\n        }\n\n        private void BtnOK_LeftClick(object sender, EventArgs e)\n        {\n            Hide();\n            OKClickedAction?.Invoke(this);\n        }\n\n        private void BtnYes_LeftClick(object sender, EventArgs e)\n        {\n            Hide();\n            YesClickedAction?.Invoke(this);\n        }\n\n        private void BtnNo_LeftClick(object sender, EventArgs e)\n        {\n            Hide();\n            NoClickedAction?.Invoke(this);\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Hide();\n            CancelClickedAction?.Invoke(this);\n        }\n\n        private void Hide()\n        {\n            if (this.Parent != null)\n                WindowManager.RemoveControl(this.Parent);\n            else\n                WindowManager.RemoveControl(this);\n        }\n\n        public void Show()\n        {\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, this);\n        }\n\n        #region Static Show methods\n\n        /// <summary>\n        /// Creates and displays a new message box with the specified caption and description.\n        /// </summary>\n        /// <param name=\"game\">The game.</param>\n        /// <param name=\"caption\">The caption/header of the message box.</param>\n        /// <param name=\"description\">The description of the message box.</param>\n        public static void Show(WindowManager windowManager, string caption, string description)\n        {\n            var panel = new DarkeningPanel(windowManager)\n            {\n                Focused = true\n            };\n\n            windowManager.AddAndInitializeControl(panel);\n\n            var msgBox = new XNAMessageBox(windowManager,\n                Renderer.GetSafeString(caption, 1), \n                Renderer.GetSafeString(description, 0), \n                XNAMessageBoxButtons.OK);\n\n            panel.AddChild(msgBox);\n            msgBox.OKClickedAction = MsgBox_OKClicked;\n            windowManager.AddAndInitializeControl(msgBox);\n            windowManager.SelectedControl = null;\n        }\n\n        private static void MsgBox_OKClicked(XNAMessageBox messageBox)\n        {\n            var parent = (DarkeningPanel)messageBox.Parent;\n            parent.Hide();\n            parent.Hidden += Parent_Hidden;\n        }\n\n        /// <summary>\n        /// Shows a message box with \"Yes\" and \"No\" being the user input options.\n        /// </summary>\n        /// <param name=\"windowManager\">The WindowManager.</param>\n        /// <param name=\"caption\">The caption of the message box.</param>\n        /// <param name=\"description\">The description in the message box.</param>\n        /// <returns>The XNAMessageBox instance that is created.</returns>\n        public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description)\n        {\n            var panel = new DarkeningPanel(windowManager)\n            {\n                Focused = true\n            };\n\n            windowManager.AddAndInitializeControl(panel);\n\n            var msgBox = new XNAMessageBox(windowManager,\n                Renderer.GetSafeString(caption, 1),\n                Renderer.GetSafeString(description, 0),\n                XNAMessageBoxButtons.YesNo);\n\n            panel.AddChild(msgBox);\n            msgBox.YesClickedAction = MsgBox_YesClicked;\n            msgBox.NoClickedAction = MsgBox_NoClicked;\n\n            return msgBox;\n        }\n\n        private static void MsgBox_NoClicked(XNAMessageBox messageBox)\n        {\n            var parent = (DarkeningPanel)messageBox.Parent;\n            parent.Hide();\n            parent.Hidden += Parent_Hidden;\n        }\n\n        private static void MsgBox_YesClicked(XNAMessageBox messageBox)\n        {\n            var parent = (DarkeningPanel)messageBox.Parent;\n            parent.Hide();\n            parent.Hidden += Parent_Hidden;\n        }\n\n        private static void Parent_Hidden(object sender, EventArgs e)\n        {\n            var darkeningPanel = (DarkeningPanel)sender;\n\n            darkeningPanel.WindowManager.RemoveControl(darkeningPanel);\n            darkeningPanel.Hidden -= Parent_Hidden;\n        }\n\n        #endregion\n    }\n\n    public enum XNAMessageBoxButtons\n    {\n        OK,\n        YesNo,\n        OKCancel\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAOptionsPanel.cs",
    "content": "﻿using ClientCore;\nusing ClientGUI.Settings;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System.Collections.Generic;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A base class for all option panels.\n    /// Handles custom game-specific panel options\n    /// defined in INI files.\n    /// </summary>\n    public abstract class XNAOptionsPanel : XNAWindowBase\n    {\n        public XNAOptionsPanel(WindowManager windowManager, \n            UserINISettings iniSettings) : base(windowManager)\n        {\n            IniSettings = iniSettings;\n        }\n\n        private readonly List<IUserSetting> userSettings = new List<IUserSetting>();\n\n        public override void Initialize()\n        {\n            ClientRectangle = new Rectangle(12, 47,\n                Parent.Width - 24,\n                Parent.Height - 94);\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            base.Initialize();\n\n            GameProcessLogic.GameProcessExited += GameProcessExited_Callback;\n        }\n\n        private void GameProcessExited_Callback()\n        {\n            foreach (IUserSetting setting in userSettings)\n            {\n                if (!setting.ResetToDefaultOnGameExit)\n                    continue;\n\n                if (setting is SettingCheckBoxBase cb)\n                    cb.Checked = cb.DefaultValue;\n                else if (setting is SettingDropDownBase dd)\n                    dd.SelectedIndex = dd.DefaultValue;\n\n                setting.Save();\n            }\n        }\n\n        /// <summary>\n        /// Parses user-defined game options from an INI file.\n        /// </summary>\n        /// <param name=\"iniFile\">The INI file.</param>\n        public void ParseUserOptions(IniFile iniFile)\n        {\n            GetAttributes(iniFile);\n            ParseExtraControls(iniFile, Name + \"ExtraControls\");\n            ReadChildControlAttributes(iniFile);\n        }\n\n        public override void AddChild(XNAControl child)\n        {\n            base.AddChild(child);\n\n            if (child is IUserSetting setting)\n                userSettings.Add(setting);\n        }\n\n        protected UserINISettings IniSettings { get; private set; }\n\n        /// <summary>\n        /// Saves the options of this panel.\n        /// <returns>A bool that determines whether the \n        /// client needs to restart for changes to apply.</returns>\n        /// </summary>\n        public virtual bool Save()\n        {\n            bool restartRequired = false;\n            foreach (var setting in userSettings)\n                restartRequired = setting.Save() || restartRequired;\n\n            return restartRequired;\n        }\n\n        /// <summary>\n        /// Refreshes the panel's settings to account for possible\n        /// changes that could affect the functionality.\n        /// </summary>\n        /// <returns>A bool that determines whether the \n        /// setting's value was changed.</returns>\n        public virtual bool RefreshPanel()\n        {\n            bool valuesChanged = false;\n            foreach (var setting in userSettings)\n            {\n                if (setting is IFileSetting fileSetting)\n                    valuesChanged = fileSetting.RefreshSetting() || valuesChanged;\n            }\n\n            return valuesChanged;\n        }\n\n        /// <summary>\n        /// Loads the options of this panel.\n        /// </summary>\n        public virtual void Load()\n        {\n            foreach (var setting in userSettings)\n                setting.Load();\n        }\n\n        /// <summary>\n        /// Enables or disables any options that should only be available when\n        /// options window was opened in main menu.\n        /// </summary>\n        /// <param name=\"enable\">If true enables options, disables if false.</param>\n        public virtual void ToggleMainMenuOnlyOptions(bool enable)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAPlayerSlotIndicator.cs",
    "content": "﻿using Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing ClientCore.Extensions;\n\nnamespace ClientGUI\n{\n    public enum PlayerSlotState\n    {\n        Empty,\n        Unavailable,\n        AI,\n        NotReady,\n        Ready,\n        InGame,\n        Warning,\n        Error\n    }\n\n    public class XNAPlayerSlotIndicator : XNAIndicator<PlayerSlotState>\n    {\n        public static new Dictionary<PlayerSlotState, Texture2D> Textures { get; set; }\n\n        public ToolTip ToolTip { get; private set; }\n\n        public XNAPlayerSlotIndicator(WindowManager windowManager) : base(windowManager, Textures) { }\n\n        public static void LoadTextures()\n        {\n            Textures = new Dictionary<PlayerSlotState, Texture2D>()\n            {\n                { PlayerSlotState.Empty, AssetLoader.LoadTextureUncached(\"statusEmpty.png\") },\n                { PlayerSlotState.Unavailable, AssetLoader.LoadTextureUncached(\"statusUnavailable.png\") },\n                { PlayerSlotState.AI, AssetLoader.LoadTextureUncached(\"statusAI.png\") },\n                { PlayerSlotState.NotReady, AssetLoader.LoadTextureUncached(\"statusClear.png\") },\n                { PlayerSlotState.Ready, AssetLoader.LoadTextureUncached(\"statusOk.png\") },\n                { PlayerSlotState.InGame, AssetLoader.LoadTextureUncached(\"statusInProgress.png\") },\n                { PlayerSlotState.Warning, AssetLoader.LoadTextureUncached(\"statusWarning.png\") },\n                { PlayerSlotState.Error, AssetLoader.LoadTextureUncached(\"statusError.png\") }\n            };\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            ToolTip = new ToolTip(WindowManager, this);\n        }\n\n        public override void SwitchTexture(PlayerSlotState key)\n        {\n            base.SwitchTexture(key);\n\n            switch (key)\n            {\n                case PlayerSlotState.Empty:\n                    ToolTip.Text = \"The slot is empty.\".L10N(\"Client:ClientGUI:SlotEmpty\");\n                    break;\n\n                case PlayerSlotState.Unavailable:\n                    ToolTip.Text = \"The slot is unavailable.\".L10N(\"Client:ClientGUI:SlotUnavailable\");\n                    break;\n\n                case PlayerSlotState.AI:\n                    ToolTip.Text = \"The player is computer-controlled.\".L10N(\"Client:ClientGUI:PlayerIsComputer\");\n                    break;\n\n                case PlayerSlotState.NotReady:\n                    ToolTip.Text = \"The player isn't ready.\".L10N(\"Client:ClientGUI:PlayerIsNotReady\");\n                    break;\n\n                case PlayerSlotState.Ready:\n                    ToolTip.Text = \"The player is ready.\".L10N(\"Client:ClientGUI:PlayerIsReady\");\n                    break;\n\n                case PlayerSlotState.InGame:\n                    ToolTip.Text = \"The player is in game.\".L10N(\"Client:ClientGUI:PlayerIsInGame\");\n                    break;\n\n                case PlayerSlotState.Warning:\n                    ToolTip.Text = \"The player has some issue(s) that may impact gameplay.\".L10N(\"Client:ClientGUI:PlayerHasIssue\");\n                    break;\n\n                case PlayerSlotState.Error:\n                    ToolTip.Text = \"There's a critical issue with the player.\".L10N(\"Client:ClientGUI:PlayerHasCriticalIssue\");\n                    break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAWindow.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing Rampastring.XNAUI;\n\nnamespace ClientGUI\n{\n    /// <summary>\n    /// A sub-window to be displayed inside the game window.\n    /// Supports easy reading of child controls' attributes from an INI file.\n    /// </summary>\n    public class XNAWindow : XNAWindowBase\n    {\n        private const string GENERIC_WINDOW_INI = \"GenericWindow.ini\";\n        private const string GENERIC_WINDOW_SECTION = \"GenericWindow\";\n        private const string EXTRA_CONTROLS = \"ExtraControls\";\n\n        public XNAWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        /// <summary>\n        /// The INI file that was used for theming this window.\n        /// </summary>\n        protected IniFile ThemeIni { get; set; }\n\n        public override float Alpha\n        {\n            get\n            {\n                return 1.0f;\n            }\n        }\n\n        protected virtual void SetAttributesFromIni()\n        {\n            if (SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($\"{Name}.ini\")).Exists)\n                GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), FormattableString.Invariant($\"{Name}.ini\"))));\n            else if (SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($\"{Name}.ini\")).Exists)\n                GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($\"{Name}.ini\"))));\n            else if (SafePath.GetFile(ProgramConstants.GetResourcePath(), GENERIC_WINDOW_INI).Exists)\n                GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), GENERIC_WINDOW_INI)));\n            else\n                GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), GENERIC_WINDOW_INI)));\n        }\n\n        /// <summary>\n        /// Reads this window's attributes from an INI file.\n        /// </summary>\n        protected virtual void GetINIAttributes(IniFile iniFile)\n        {\n            ThemeIni = iniFile;\n\n            List<string> keys = iniFile.GetSectionKeys(Name);\n\n            if (keys != null)\n            {\n                foreach (string key in keys)\n                    ParseINIAttribute(iniFile, key, iniFile.GetStringValue(Name, key, String.Empty));\n            }\n            else\n            {\n                keys = iniFile.GetSectionKeys(GENERIC_WINDOW_SECTION);\n\n                if (keys != null)\n                {\n                    foreach (string key in keys)\n                        ParseINIAttribute(iniFile, key, iniFile.GetStringValue(GENERIC_WINDOW_SECTION, key, String.Empty));\n                }\n            }\n\n            ParseExtraControls(iniFile, EXTRA_CONTROLS);\n            ReadChildControlAttributes(iniFile);\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            SetAttributesFromIni();\n        }\n    }\n}\n"
  },
  {
    "path": "ClientGUI/XNAWindowBase.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System.Linq;\n\nnamespace ClientGUI\n{\n    public class XNAWindowBase : XNAPanel\n    {\n        public XNAWindowBase(WindowManager windowManager) : base(windowManager)\n        {\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.TILED;\n        }\n\n        /// <summary>\n        /// Reads extra control information from a specific section of an INI file.\n        /// </summary>\n        /// <param name=\"iniFile\">The INI file.</param>\n        /// <param name=\"sectionName\">The section.</param>\n        protected virtual void ParseExtraControls(IniFile iniFile, string sectionName)\n        {\n            var section = iniFile.GetSection(sectionName);\n\n            if (section == null)\n                return;\n\n            foreach (var kvp in section.Keys)\n            {\n                string[] parts = kvp.Value.Split(':');\n                if (parts.Length != 2)\n                    throw new ClientConfigurationException(\"Invalid ExtraControl specified in \" + Name + \": \" + kvp.Value);\n\n                if (!Children.Any(child => child.Name == parts[0]))\n                {\n                    XNAControl control = ClientGUICreator.GetXnaControl(parts[1]);\n                    control.Name = parts[0];\n                    control.DrawOrder = -Children.Count;\n                    AddChild(control);\n                }\n            }\n        }\n\n        protected virtual void ReadChildControlAttributes(IniFile iniFile)\n        {\n            foreach (XNAControl child in Children)\n            {\n                if (!(typeof(XNAWindowBase).IsAssignableFrom(child.GetType())))\n                    child.GetAttributes(iniFile);\n            }\n        }\n\n        /// <summary>\n        /// Creates a control with a given name, using the specified GUI creator\n        /// and control type name.\n        /// </summary>\n        /// <param name=\"guiCreator\">The <see cref=\"GUICreator\"/> to use.</param>\n        /// <param name=\"controlTypeName\">The name of the control's type.</param>\n        /// <param name=\"controlName\">The name of the created control.</param>\n        /// <returns>The created control.</returns>\n        protected virtual XNAControl CreateControl(GUICreator guiCreator, string controlTypeName, string controlName)\n        {\n            var control = guiCreator.CreateControl(WindowManager, controlTypeName);\n            control.Name = controlName;\n            control.DrawOrder = -Children.Count;\n            AddChild(control);\n            return control;\n        }\n    }\n}\n"
  },
  {
    "path": "ClientUpdater/ClientUpdater.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <Title>CnCNet.ClientUpdater</Title>\n    <Description>CnCNet Client Updater Library</Description>\n    <Product>CnCNet.ClientUpdater</Product>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNet.WebApi.Client\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ClientCore\\ClientCore.csproj\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "ClientUpdater/Compression/Common/CRC.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\n// Common/CRC.cs\n\nnamespace SevenZip\n{\n\tclass CRC\n\t{\n\t\tpublic static readonly uint[] Table;\n\n\t\tstatic CRC()\n\t\t{\n\t\t\tTable = new uint[256];\n\t\t\tconst uint kPoly = 0xEDB88320;\n\t\t\tfor (uint i = 0; i < 256; i++)\n\t\t\t{\n\t\t\t\tuint r = i;\n\t\t\t\tfor (int j = 0; j < 8; j++)\n\t\t\t\t\tif ((r & 1) != 0)\n\t\t\t\t\t\tr = (r >> 1) ^ kPoly;\n\t\t\t\t\telse\n\t\t\t\t\t\tr >>= 1;\n\t\t\t\tTable[i] = r;\n\t\t\t}\n\t\t}\n\n\t\tuint _value = 0xFFFFFFFF;\n\n\t\tpublic void Init() { _value = 0xFFFFFFFF; }\n\n\t\tpublic void UpdateByte(byte b)\n\t\t{\n\t\t\t_value = Table[(((byte)(_value)) ^ b)] ^ (_value >> 8);\n\t\t}\n\n\t\tpublic void Update(byte[] data, uint offset, uint size)\n\t\t{\n\t\t\tfor (uint i = 0; i < size; i++)\n\t\t\t\t_value = Table[(((byte)(_value)) ^ data[offset + i])] ^ (_value >> 8);\n\t\t}\n\n\t\tpublic uint GetDigest() { return _value ^ 0xFFFFFFFF; }\n\n\t\tstatic uint CalculateDigest(byte[] data, uint offset, uint size)\n\t\t{\n\t\t\tCRC crc = new CRC();\n\t\t\t// crc.Init();\n\t\t\tcrc.Update(data, offset, size);\n\t\t\treturn crc.GetDigest();\n\t\t}\n\n\t\tstatic bool VerifyDigest(uint digest, byte[] data, uint offset, uint size)\n\t\t{\n\t\t\treturn (CalculateDigest(data, offset, size) == digest);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/Common/CommandLineParser.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// CommandLineParser.cs\n\nusing System;\nusing System.Collections;\n\nnamespace SevenZip.CommandLineParser\n{\n\tpublic enum SwitchType\n\t{\n\t\tSimple,\n\t\tPostMinus,\n\t\tLimitedPostString,\n\t\tUnLimitedPostString,\n\t\tPostChar\n\t}\n\n\tpublic class SwitchForm\n\t{\n\t\tpublic string IDString;\n\t\tpublic SwitchType Type;\n\t\tpublic bool Multi;\n\t\tpublic int MinLen;\n\t\tpublic int MaxLen;\n\t\tpublic string PostCharSet;\n\n\t\tpublic SwitchForm(string idString, SwitchType type, bool multi,\n\t\t\tint minLen, int maxLen, string postCharSet)\n\t\t{\n\t\t\tIDString = idString;\n\t\t\tType = type;\n\t\t\tMulti = multi;\n\t\t\tMinLen = minLen;\n\t\t\tMaxLen = maxLen;\n\t\t\tPostCharSet = postCharSet;\n\t\t}\n\t\tpublic SwitchForm(string idString, SwitchType type, bool multi, int minLen):\n\t\t\tthis(idString, type, multi, minLen, 0, \"\")\n\t\t{\n\t\t}\n\t\tpublic SwitchForm(string idString, SwitchType type, bool multi):\n\t\t\tthis(idString, type, multi, 0)\n\t\t{\n\t\t}\n\t}\n\n\tpublic class SwitchResult\n\t{\n\t\tpublic bool ThereIs;\n\t\tpublic bool WithMinus;\n\t\tpublic ArrayList PostStrings = new ArrayList();\n\t\tpublic int PostCharIndex;\n\t\tpublic SwitchResult()\n\t\t{\n\t\t\tThereIs = false;\n\t\t}\n\t}\n\n\tpublic class Parser\n\t{\n\t\tpublic ArrayList NonSwitchStrings = new ArrayList();\n\t\tSwitchResult[] _switches;\n\n\t\tpublic Parser(int numSwitches)\n\t\t{\n\t\t\t_switches = new SwitchResult[numSwitches];\n\t\t\tfor (int i = 0; i < numSwitches; i++)\n\t\t\t\t_switches[i] = new SwitchResult();\n\t\t}\n\n\t\tbool ParseString(string srcString, SwitchForm[] switchForms)\n\t\t{\n\t\t\tint len = srcString.Length;\n\t\t\tif (len == 0)\n\t\t\t\treturn false;\n\t\t\tint pos = 0;\n\t\t\tif (!IsItSwitchChar(srcString[pos]))\n\t\t\t\treturn false;\n\t\t\twhile (pos < len)\n\t\t\t{\n\t\t\t\tif (IsItSwitchChar(srcString[pos]))\n\t\t\t\t\tpos++;\n\t\t\t\tconst int kNoLen = -1;\n\t\t\t\tint matchedSwitchIndex = 0;\n\t\t\t\tint maxLen = kNoLen;\n\t\t\t\tfor (int switchIndex = 0; switchIndex < _switches.Length; switchIndex++)\n\t\t\t\t{\n\t\t\t\t\tint switchLen = switchForms[switchIndex].IDString.Length;\n\t\t\t\t\tif (switchLen <= maxLen || pos + switchLen > len)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tif (String.Compare(switchForms[switchIndex].IDString, 0,\n\t\t\t\t\t\t\tsrcString, pos, switchLen, true) == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tmatchedSwitchIndex = switchIndex;\n\t\t\t\t\t\tmaxLen = switchLen;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (maxLen == kNoLen)\n\t\t\t\t\tthrow new Exception(\"maxLen == kNoLen\");\n\t\t\t\tSwitchResult matchedSwitch = _switches[matchedSwitchIndex];\n\t\t\t\tSwitchForm switchForm = switchForms[matchedSwitchIndex];\n\t\t\t\tif ((!switchForm.Multi) && matchedSwitch.ThereIs)\n\t\t\t\t\tthrow new Exception(\"switch must be single\");\n\t\t\t\tmatchedSwitch.ThereIs = true;\n\t\t\t\tpos += maxLen;\n\t\t\t\tint tailSize = len - pos;\n\t\t\t\tSwitchType type = switchForm.Type;\n\t\t\t\tswitch (type)\n\t\t\t\t{\n\t\t\t\t\tcase SwitchType.PostMinus:\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (tailSize == 0)\n\t\t\t\t\t\t\t\tmatchedSwitch.WithMinus = false;\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tmatchedSwitch.WithMinus = (srcString[pos] == kSwitchMinus);\n\t\t\t\t\t\t\t\tif (matchedSwitch.WithMinus)\n\t\t\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\tcase SwitchType.PostChar:\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (tailSize < switchForm.MinLen)\n\t\t\t\t\t\t\t\tthrow new Exception(\"switch is not full\");\n\t\t\t\t\t\t\tstring charSet = switchForm.PostCharSet;\n\t\t\t\t\t\t\tconst int kEmptyCharValue = -1;\n\t\t\t\t\t\t\tif (tailSize == 0)\n\t\t\t\t\t\t\t\tmatchedSwitch.PostCharIndex = kEmptyCharValue;\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tint index = charSet.IndexOf(srcString[pos]);\n\t\t\t\t\t\t\t\tif (index < 0)\n\t\t\t\t\t\t\t\t\tmatchedSwitch.PostCharIndex = kEmptyCharValue;\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tmatchedSwitch.PostCharIndex = index;\n\t\t\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\tcase SwitchType.LimitedPostString:\n\t\t\t\t\tcase SwitchType.UnLimitedPostString:\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tint minLen = switchForm.MinLen;\n\t\t\t\t\t\t\tif (tailSize < minLen)\n\t\t\t\t\t\t\t\tthrow new Exception(\"switch is not full\");\n\t\t\t\t\t\t\tif (type == SwitchType.UnLimitedPostString)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tmatchedSwitch.PostStrings.Add(srcString.Substring(pos));\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tString stringSwitch = srcString.Substring(pos, minLen);\n\t\t\t\t\t\t\tpos += minLen;\n\t\t\t\t\t\t\tfor (int i = minLen; i < switchForm.MaxLen && pos < len; i++, pos++)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tchar c = srcString[pos];\n\t\t\t\t\t\t\t\tif (IsItSwitchChar(c))\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tstringSwitch += c;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmatchedSwitch.PostStrings.Add(stringSwitch);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\n\t\t}\n\n\t\tpublic void ParseStrings(SwitchForm[] switchForms, string[] commandStrings)\n\t\t{\n\t\t\tint numCommandStrings = commandStrings.Length;\n\t\t\tbool stopSwitch = false;\n\t\t\tfor (int i = 0; i < numCommandStrings; i++)\n\t\t\t{\n\t\t\t\tstring s = commandStrings[i];\n\t\t\t\tif (stopSwitch)\n\t\t\t\t\tNonSwitchStrings.Add(s);\n\t\t\t\telse\n\t\t\t\t\tif (s == kStopSwitchParsing)\n\t\t\t\t\tstopSwitch = true;\n\t\t\t\telse\n\t\t\t\t\tif (!ParseString(s, switchForms))\n\t\t\t\t\tNonSwitchStrings.Add(s);\n\t\t\t}\n\t\t}\n\n\t\tpublic SwitchResult this[int index] { get { return _switches[index]; } }\n\n\t\tpublic static int ParseCommand(CommandForm[] commandForms, string commandString,\n\t\t\tout string postString)\n\t\t{\n\t\t\tfor (int i = 0; i < commandForms.Length; i++)\n\t\t\t{\n\t\t\t\tstring id = commandForms[i].IDString;\n\t\t\t\tif (commandForms[i].PostStringMode)\n\t\t\t\t{\n\t\t\t\t\tif (commandString.IndexOf(id) == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tpostString = commandString.Substring(id.Length);\n\t\t\t\t\t\treturn i;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tif (commandString == id)\n\t\t\t\t{\n\t\t\t\t\tpostString = \"\";\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tpostString = \"\";\n\t\t\treturn -1;\n\t\t}\n\n\t\tstatic bool ParseSubCharsCommand(int numForms, CommandSubCharsSet[] forms,\n\t\t\tstring commandString, ArrayList indices)\n\t\t{\n\t\t\tindices.Clear();\n\t\t\tint numUsedChars = 0;\n\t\t\tfor (int i = 0; i < numForms; i++)\n\t\t\t{\n\t\t\t\tCommandSubCharsSet charsSet = forms[i];\n\t\t\t\tint currentIndex = -1;\n\t\t\t\tint len = charsSet.Chars.Length;\n\t\t\t\tfor (int j = 0; j < len; j++)\n\t\t\t\t{\n\t\t\t\t\tchar c = charsSet.Chars[j];\n\t\t\t\t\tint newIndex = commandString.IndexOf(c);\n\t\t\t\t\tif (newIndex >= 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tif (currentIndex >= 0)\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\tif (commandString.IndexOf(c, newIndex + 1) >= 0)\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\tcurrentIndex = j;\n\t\t\t\t\t\tnumUsedChars++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (currentIndex == -1 && !charsSet.EmptyAllowed)\n\t\t\t\t\treturn false;\n\t\t\t\tindices.Add(currentIndex);\n\t\t\t}\n\t\t\treturn (numUsedChars == commandString.Length);\n\t\t}\n\t\tconst char kSwitchID1 = '-';\n\t\tconst char kSwitchID2 = '/';\n\n\t\tconst char kSwitchMinus = '-';\n\t\tconst string kStopSwitchParsing = \"--\";\n\n\t\tstatic bool IsItSwitchChar(char c)\n\t\t{\n\t\t\treturn (c == kSwitchID1 || c == kSwitchID2);\n\t\t}\n\t}\n\n\tpublic class CommandForm\n\t{\n\t\tpublic string IDString = \"\";\n\t\tpublic bool PostStringMode = false;\n\t\tpublic CommandForm(string idString, bool postStringMode)\n\t\t{\n\t\t\tIDString = idString;\n\t\t\tPostStringMode = postStringMode;\n\t\t}\n\t}\n\n\tclass CommandSubCharsSet\n\t{\n\t\tpublic string Chars = \"\";\n\t\tpublic bool EmptyAllowed = false;\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/Common/InBuffer.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// InBuffer.cs\n\nnamespace SevenZip.Buffer\n{\n\tpublic class InBuffer\n\t{\n\t\tbyte[] m_Buffer;\n\t\tuint m_Pos;\n\t\tuint m_Limit;\n\t\tuint m_BufferSize;\n\t\tSystem.IO.Stream m_Stream;\n\t\tbool m_StreamWasExhausted;\n\t\tulong m_ProcessedSize;\n\n\t\tpublic InBuffer(uint bufferSize)\n\t\t{\n\t\t\tm_Buffer = new byte[bufferSize];\n\t\t\tm_BufferSize = bufferSize;\n\t\t}\n\n\t\tpublic void Init(System.IO.Stream stream)\n\t\t{\n\t\t\tm_Stream = stream;\n\t\t\tm_ProcessedSize = 0;\n\t\t\tm_Limit = 0;\n\t\t\tm_Pos = 0;\n\t\t\tm_StreamWasExhausted = false;\n\t\t}\n\n\t\tpublic bool ReadBlock()\n\t\t{\n\t\t\tif (m_StreamWasExhausted)\n\t\t\t\treturn false;\n\t\t\tm_ProcessedSize += m_Pos;\n\t\t\tint aNumProcessedBytes = m_Stream.Read(m_Buffer, 0, (int)m_BufferSize);\n\t\t\tm_Pos = 0;\n\t\t\tm_Limit = (uint)aNumProcessedBytes;\n\t\t\tm_StreamWasExhausted = (aNumProcessedBytes == 0);\n\t\t\treturn (!m_StreamWasExhausted);\n\t\t}\n\n\n\t\tpublic void ReleaseStream()\n\t\t{\n\t\t\t// m_Stream.Close(); \n\t\t\tm_Stream = null;\n\t\t}\n\n\t\tpublic bool ReadByte(byte b) // check it\n\t\t{\n\t\t\tif (m_Pos >= m_Limit)\n\t\t\t\tif (!ReadBlock())\n\t\t\t\t\treturn false;\n\t\t\tb = m_Buffer[m_Pos++];\n\t\t\treturn true;\n\t\t}\n\n\t\tpublic byte ReadByte()\n\t\t{\n\t\t\t// return (byte)m_Stream.ReadByte();\n\t\t\tif (m_Pos >= m_Limit)\n\t\t\t\tif (!ReadBlock())\n\t\t\t\t\treturn 0xFF;\n\t\t\treturn m_Buffer[m_Pos++];\n\t\t}\n\n\t\tpublic ulong GetProcessedSize()\n\t\t{\n\t\t\treturn m_ProcessedSize + m_Pos;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/Common/OutBuffer.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// OutBuffer.cs\n\nnamespace SevenZip.Buffer\n{\n\tpublic class OutBuffer\n\t{\n\t\tbyte[] m_Buffer;\n\t\tuint m_Pos;\n\t\tuint m_BufferSize;\n\t\tSystem.IO.Stream m_Stream;\n\t\tulong m_ProcessedSize;\n\n\t\tpublic OutBuffer(uint bufferSize)\n\t\t{\n\t\t\tm_Buffer = new byte[bufferSize];\n\t\t\tm_BufferSize = bufferSize;\n\t\t}\n\n\t\tpublic void SetStream(System.IO.Stream stream) { m_Stream = stream; }\n\t\tpublic void FlushStream() { m_Stream.Flush(); }\n\t\tpublic void CloseStream() { m_Stream.Close(); }\n\t\tpublic void ReleaseStream() { m_Stream = null; }\n\n\t\tpublic void Init()\n\t\t{\n\t\t\tm_ProcessedSize = 0;\n\t\t\tm_Pos = 0;\n\t\t}\n\n\t\tpublic void WriteByte(byte b)\n\t\t{\n\t\t\tm_Buffer[m_Pos++] = b;\n\t\t\tif (m_Pos >= m_BufferSize)\n\t\t\t\tFlushData();\n\t\t}\n\n\t\tpublic void FlushData()\n\t\t{\n\t\t\tif (m_Pos == 0)\n\t\t\t\treturn;\n\t\t\tm_Stream.Write(m_Buffer, 0, (int)m_Pos);\n\t\t\tm_Pos = 0;\n\t\t}\n\n\t\tpublic ulong GetProcessedSize() { return m_ProcessedSize + m_Pos; }\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/CompressionHelper.cs",
    "content": "﻿// Copyright 2022-2024 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater.Compression;\n\nusing System;\nusing System.Buffers.Binary;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing SevenZip.Compression.LZMA;\n\n/// <summary>\n/// LZMA compression helper.\n/// </summary>\npublic static class CompressionHelper\n{\n    /// <summary>\n    /// Compress file using LZMA.\n    /// </summary>\n    /// <param name=\"inputFilename\">Input file path.</param>\n    /// <param name=\"outputFilename\">Output file path.</param>\n    public static async ValueTask CompressFileAsync(string inputFilename, string outputFilename, CancellationToken cancellationToken = default)\n    {\n        var encoder = new Encoder(cancellationToken);\n        var inputStream = new FileStream(inputFilename, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);\n\n        using (inputStream)\n        {\n            var outputStream = new FileStream(outputFilename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n            using (outputStream)\n            {\n                encoder.WriteCoderProperties(outputStream);\n                byte[] lengthBytes = new byte[sizeof(long)];\n                BinaryPrimitives.WriteInt64LittleEndian(lengthBytes, inputStream.Length);\n                await outputStream.WriteAsync(lengthBytes.AsMemory(0, sizeof(long)), cancellationToken).ConfigureAwait(false);\n                encoder.Code(inputStream, outputStream, inputStream.Length, outputStream.Length, null);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Decompress file using LZMA.\n    /// </summary>\n    /// <param name=\"inputFilename\">Input file path.</param>\n    /// <param name=\"outputFilename\">Output file path.</param>\n    public static async ValueTask DecompressFileAsync(string inputFilename, string outputFilename, CancellationToken cancellationToken = default)\n    {\n        var decoder = new Decoder(cancellationToken);\n\n        var inputStream = new FileStream(inputFilename, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);\n\n        using (inputStream)\n        {\n            var outputStream = new FileStream(outputFilename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n            using (outputStream)\n            {\n                byte[] properties = new byte[5];\n                byte[] fileLengthArray = new byte[sizeof(long)];\n\n                await inputStream.ReadAsync(properties, cancellationToken).ConfigureAwait(false);\n                await inputStream.ReadAsync(fileLengthArray, cancellationToken).ConfigureAwait(false);\n\n                long fileLength = BinaryPrimitives.ReadInt64LittleEndian(fileLengthArray);\n\n                decoder.SetDecoderProperties(properties);\n                decoder.Code(inputStream, outputStream, inputStream.Length, fileLength, null);\n            }\n        }\n    }\n}"
  },
  {
    "path": "ClientUpdater/Compression/ICoder.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1570, CS1591\n\n// ICoder.h\n\nusing System;\n\nnamespace SevenZip\n{\n\t/// <summary>\n\t/// The exception that is thrown when an error in input stream occurs during decoding.\n\t/// </summary>\n\tclass DataErrorException : ApplicationException\n\t{\n\t\tpublic DataErrorException(): base(\"Data Error\") { }\n\t}\n\n\t/// <summary>\n\t/// The exception that is thrown when the value of an argument is outside the allowable range.\n\t/// </summary>\n\tclass InvalidParamException : ApplicationException\n\t{\n\t\tpublic InvalidParamException(): base(\"Invalid Parameter\") { }\n\t}\n\n\tpublic interface ICodeProgress\n\t{\n\t\t/// <summary>\n\t\t/// Callback progress.\n\t\t/// </summary>\n\t\t/// <param name=\"inSize\">\n\t\t/// input size. -1 if unknown.\n\t\t/// </param>\n\t\t/// <param name=\"outSize\">\n\t\t/// output size. -1 if unknown.\n\t\t/// </param>\n\t\tvoid SetProgress(Int64 inSize, Int64 outSize);\n\t};\n\n\tpublic interface ICoder\n\t{\n\t\t/// <summary>\n\t\t/// Codes streams.\n\t\t/// </summary>\n\t\t/// <param name=\"inStream\">\n\t\t/// input Stream.\n\t\t/// </param>\n\t\t/// <param name=\"outStream\">\n\t\t/// output Stream.\n\t\t/// </param>\n\t\t/// <param name=\"inSize\">\n\t\t/// input Size. -1 if unknown.\n\t\t/// </param>\n\t\t/// <param name=\"outSize\">\n\t\t/// output Size. -1 if unknown.\n\t\t/// </param>\n\t\t/// <param name=\"progress\">\n\t\t/// callback progress reference.\n\t\t/// </param>\n\t\t/// <exception cref=\"SevenZip.DataErrorException\">\n\t\t/// if input stream is not valid\n\t\t/// </exception>\n\t\tvoid Code(System.IO.Stream inStream, System.IO.Stream outStream,\n\t\t\tInt64 inSize, Int64 outSize, ICodeProgress progress);\n\t};\n\n\t/*\n\tpublic interface ICoder2\n\t{\n\t\t void Code(ISequentialInStream []inStreams,\n\t\t\t\tconst UInt64 []inSizes, \n\t\t\t\tISequentialOutStream []outStreams, \n\t\t\t\tUInt64 []outSizes,\n\t\t\t\tICodeProgress progress);\n\t};\n  */\n\n\t/// <summary>\n\t/// Provides the fields that represent properties idenitifiers for compressing.\n\t/// </summary>\n\tpublic enum CoderPropID\n\t{\n\t\t/// <summary>\n\t\t/// Specifies default property.\n\t\t/// </summary>\n\t\tDefaultProp = 0,\n\t\t/// <summary>\n\t\t/// Specifies size of dictionary.\n\t\t/// </summary>\n\t\tDictionarySize,\n\t\t/// <summary>\n\t\t/// Specifies size of memory for PPM*.\n\t\t/// </summary>\n\t\tUsedMemorySize,\n\t\t/// <summary>\n\t\t/// Specifies order for PPM methods.\n\t\t/// </summary>\n\t\tOrder,\n\t\t/// <summary>\n\t\t/// Specifies Block Size.\n\t\t/// </summary>\n\t\tBlockSize,\n\t\t/// <summary>\n\t\t/// Specifies number of postion state bits for LZMA (0 <= x <= 4).\n\t\t/// </summary>\n\t\tPosStateBits,\n\t\t/// <summary>\n\t\t/// Specifies number of literal context bits for LZMA (0 <= x <= 8).\n\t\t/// </summary>\n\t\tLitContextBits,\n\t\t/// <summary>\n\t\t/// Specifies number of literal position bits for LZMA (0 <= x <= 4).\n\t\t/// </summary>\n\t\tLitPosBits,\n\t\t/// <summary>\n\t\t/// Specifies number of fast bytes for LZ*.\n\t\t/// </summary>\n\t\tNumFastBytes,\n\t\t/// <summary>\n\t\t/// Specifies match finder. LZMA: \"BT2\", \"BT4\" or \"BT4B\".\n\t\t/// </summary>\n\t\tMatchFinder,\n\t\t/// <summary>\n\t\t/// Specifies the number of match finder cyckes.\n\t\t/// </summary>\n\t\tMatchFinderCycles,\n\t\t/// <summary>\n\t\t/// Specifies number of passes.\n\t\t/// </summary>\n\t\tNumPasses,\n\t\t/// <summary>\n\t\t/// Specifies number of algorithm.\n\t\t/// </summary>\n\t\tAlgorithm,\n\t\t/// <summary>\n\t\t/// Specifies the number of threads.\n\t\t/// </summary>\n\t\tNumThreads,\n\t\t/// <summary>\n\t\t/// Specifies mode with end marker.\n\t\t/// </summary>\n\t\tEndMarker\n\t};\n\n\n\tpublic interface ISetCoderProperties\n\t{\n\t\tvoid SetCoderProperties(CoderPropID[] propIDs, object[] properties);\n\t};\n\n\tpublic interface IWriteCoderProperties\n\t{\n\t\tvoid WriteCoderProperties(System.IO.Stream outStream);\n\t}\n\n\tpublic interface ISetDecoderProperties\n\t{\n\t\tvoid SetDecoderProperties(byte[] properties);\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZ/IMatchFinder.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\n// IMatchFinder.cs\n\nusing System;\n\nnamespace SevenZip.Compression.LZ\n{\n\tinterface IInWindowStream\n\t{\n\t\tvoid SetStream(System.IO.Stream inStream);\n\t\tvoid Init();\n\t\tvoid ReleaseStream();\n\t\tByte GetIndexByte(Int32 index);\n\t\tUInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit);\n\t\tUInt32 GetNumAvailableBytes();\n\t}\n\n\tinterface IMatchFinder : IInWindowStream\n\t{\n\t\tvoid Create(UInt32 historySize, UInt32 keepAddBufferBefore,\n\t\t\t\tUInt32 matchMaxLen, UInt32 keepAddBufferAfter);\n\t\tUInt32 GetMatches(UInt32[] distances);\n\t\tvoid Skip(UInt32 num);\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZ/LzBinTree.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// LzBinTree.cs\n\nusing System;\n\nnamespace SevenZip.Compression.LZ\n{\n\tpublic class BinTree : InWindow, IMatchFinder\n\t{\n\t\tUInt32 _cyclicBufferPos;\n\t\tUInt32 _cyclicBufferSize = 0;\n\t\tUInt32 _matchMaxLen;\n\n\t\tUInt32[] _son;\n\t\tUInt32[] _hash;\n\n\t\tUInt32 _cutValue = 0xFF;\n\t\tUInt32 _hashMask;\n\t\tUInt32 _hashSizeSum = 0;\n\n\t\tbool HASH_ARRAY = true;\n\n\t\tconst UInt32 kHash2Size = 1 << 10;\n\t\tconst UInt32 kHash3Size = 1 << 16;\n\t\tconst UInt32 kBT2HashSize = 1 << 16;\n\t\tconst UInt32 kStartMaxLen = 1;\n\t\tconst UInt32 kHash3Offset = kHash2Size;\n\t\tconst UInt32 kEmptyHashValue = 0;\n\t\tconst UInt32 kMaxValForNormalize = ((UInt32)1 << 31) - 1;\n\t\n\t\tUInt32 kNumHashDirectBytes = 0;\n\t\tUInt32 kMinMatchCheck = 4;\n\t\tUInt32 kFixHashSize = kHash2Size + kHash3Size;\n\t\t\n\t\tpublic void SetType(int numHashBytes)\n\t\t{\n\t\t\tHASH_ARRAY = (numHashBytes > 2);\n\t\t\tif (HASH_ARRAY)\n\t\t\t{\n\t\t\t\tkNumHashDirectBytes = 0;\n\t\t\t\tkMinMatchCheck = 4;\n\t\t\t\tkFixHashSize = kHash2Size + kHash3Size;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tkNumHashDirectBytes = 2;\n\t\t\t\tkMinMatchCheck = 2 + 1;\n\t\t\t\tkFixHashSize = 0;\n\t\t\t}\n\t\t}\n\n\t\tpublic new void SetStream(System.IO.Stream stream) { base.SetStream(stream); }\n\t\tpublic new void ReleaseStream() { base.ReleaseStream(); }\n\t\t\n\t\tpublic new void Init()\n\t\t{\n\t\t\tbase.Init();\n\t\t\tfor (UInt32 i = 0; i < _hashSizeSum; i++)\n\t\t\t\t_hash[i] = kEmptyHashValue;\n\t\t\t_cyclicBufferPos = 0;\n\t\t\tReduceOffsets(-1);\n\t\t}\n\n\t\tpublic new void MovePos()\n\t\t{\n\t\t\tif (++_cyclicBufferPos >= _cyclicBufferSize)\n\t\t\t\t_cyclicBufferPos = 0;\n\t\t\tbase.MovePos();\n\t\t\tif (_pos == kMaxValForNormalize)\n\t\t\t\tNormalize();\n\t\t}\n\n\t\tpublic new Byte GetIndexByte(Int32 index) { return base.GetIndexByte(index); }\n\n\t\tpublic new UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit)\n\t\t{ return base.GetMatchLen(index, distance, limit); }\n\n\t\tpublic new UInt32 GetNumAvailableBytes() { return base.GetNumAvailableBytes(); }\n\n\t\tpublic void Create(UInt32 historySize, UInt32 keepAddBufferBefore,\n\t\t\t\tUInt32 matchMaxLen, UInt32 keepAddBufferAfter)\n\t\t{\n\t\t\tif (historySize > kMaxValForNormalize - 256)\n\t\t\t\tthrow new Exception();\n\t\t\t_cutValue = 16 + (matchMaxLen >> 1);\n\t\t\t\t\n\t\t\tUInt32 windowReservSize = (historySize + keepAddBufferBefore +\n\t\t\t\t\tmatchMaxLen + keepAddBufferAfter) / 2 + 256;\n\n\t\t\tbase.Create(historySize + keepAddBufferBefore, matchMaxLen + keepAddBufferAfter, windowReservSize);\n\n\t\t\t_matchMaxLen = matchMaxLen;\n\n\t\t\tUInt32 cyclicBufferSize = historySize + 1;\n\t\t\tif (_cyclicBufferSize != cyclicBufferSize)\n\t\t\t\t_son = new UInt32[(_cyclicBufferSize = cyclicBufferSize) * 2];\n\n\t\t\tUInt32 hs = kBT2HashSize;\n\n\t\t\tif (HASH_ARRAY)\n\t\t\t{\n\t\t\t\ths = historySize - 1;\n\t\t\t\ths |= (hs >> 1);\n\t\t\t\ths |= (hs >> 2);\n\t\t\t\ths |= (hs >> 4);\n\t\t\t\ths |= (hs >> 8);\n\t\t\t\ths >>= 1;\n\t\t\t\ths |= 0xFFFF;\n\t\t\t\tif (hs > (1 << 24))\n\t\t\t\t\ths >>= 1;\n\t\t\t\t_hashMask = hs;\n\t\t\t\ths++;\n\t\t\t\ths += kFixHashSize;\n\t\t\t}\n\t\t\tif (hs != _hashSizeSum)\n\t\t\t\t_hash = new UInt32[_hashSizeSum = hs];\n\t\t}\n\n\t\tpublic UInt32 GetMatches(UInt32[] distances)\n\t\t{\n\t\t\tUInt32 lenLimit;\n\t\t\tif (_pos + _matchMaxLen <= _streamPos)\n\t\t\t\tlenLimit = _matchMaxLen;\n\t\t\telse\n\t\t\t{\n\t\t\t\tlenLimit = _streamPos - _pos;\n\t\t\t\tif (lenLimit < kMinMatchCheck)\n\t\t\t\t{\n\t\t\t\t\tMovePos();\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tUInt32 offset = 0;\n\t\t\tUInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0;\n\t\t\tUInt32 cur = _bufferOffset + _pos;\n\t\t\tUInt32 maxLen = kStartMaxLen; // to avoid items for len < hashSize;\n\t\t\tUInt32 hashValue, hash2Value = 0, hash3Value = 0;\n\n\t\t\tif (HASH_ARRAY)\n\t\t\t{\n\t\t\t\tUInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1];\n\t\t\t\thash2Value = temp & (kHash2Size - 1);\n\t\t\t\ttemp ^= ((UInt32)(_bufferBase[cur + 2]) << 8);\n\t\t\t\thash3Value = temp & (kHash3Size - 1);\n\t\t\t\thashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask;\n\t\t\t}\n\t\t\telse\n\t\t\t\thashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8);\n\n\t\t\tUInt32 curMatch = _hash[kFixHashSize + hashValue];\n\t\t\tif (HASH_ARRAY)\n\t\t\t{\n\t\t\t\tUInt32 curMatch2 = _hash[hash2Value];\n\t\t\t\tUInt32 curMatch3 = _hash[kHash3Offset + hash3Value];\n\t\t\t\t_hash[hash2Value] = _pos;\n\t\t\t\t_hash[kHash3Offset + hash3Value] = _pos;\n\t\t\t\tif (curMatch2 > matchMinPos)\n\t\t\t\t\tif (_bufferBase[_bufferOffset + curMatch2] == _bufferBase[cur])\n\t\t\t\t\t{\n\t\t\t\t\t\tdistances[offset++] = maxLen = 2;\n\t\t\t\t\t\tdistances[offset++] = _pos - curMatch2 - 1;\n\t\t\t\t\t}\n\t\t\t\tif (curMatch3 > matchMinPos)\n\t\t\t\t\tif (_bufferBase[_bufferOffset + curMatch3] == _bufferBase[cur])\n\t\t\t\t\t{\n\t\t\t\t\t\tif (curMatch3 == curMatch2)\n\t\t\t\t\t\t\toffset -= 2;\n\t\t\t\t\t\tdistances[offset++] = maxLen = 3;\n\t\t\t\t\t\tdistances[offset++] = _pos - curMatch3 - 1;\n\t\t\t\t\t\tcurMatch2 = curMatch3;\n\t\t\t\t\t}\n\t\t\t\tif (offset != 0 && curMatch2 == curMatch)\n\t\t\t\t{\n\t\t\t\t\toffset -= 2;\n\t\t\t\t\tmaxLen = kStartMaxLen;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_hash[kFixHashSize + hashValue] = _pos;\n\n\t\t\tUInt32 ptr0 = (_cyclicBufferPos << 1) + 1;\n\t\t\tUInt32 ptr1 = (_cyclicBufferPos << 1);\n\n\t\t\tUInt32 len0, len1;\n\t\t\tlen0 = len1 = kNumHashDirectBytes;\n\t\t\t\n\t\t\tif (kNumHashDirectBytes != 0)\n\t\t\t{\n\t\t\t\tif (curMatch > matchMinPos)\n\t\t\t\t{\n\t\t\t\t\tif (_bufferBase[_bufferOffset + curMatch + kNumHashDirectBytes] !=\n\t\t\t\t\t\t\t_bufferBase[cur + kNumHashDirectBytes])\n\t\t\t\t\t{\n\t\t\t\t\t\tdistances[offset++] = maxLen = kNumHashDirectBytes;\n\t\t\t\t\t\tdistances[offset++] = _pos - curMatch - 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tUInt32 count = _cutValue;\n\t\t\t\n\t\t\twhile(true)\n\t\t\t{\n\t\t\t\tif(curMatch <= matchMinPos || count-- == 0)\n\t\t\t\t{\n\t\t\t\t\t_son[ptr0] = _son[ptr1] = kEmptyHashValue;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tUInt32 delta = _pos - curMatch;\n\t\t\t\tUInt32 cyclicPos = ((delta <= _cyclicBufferPos) ?\n\t\t\t\t\t\t\t(_cyclicBufferPos - delta) :\n\t\t\t\t\t\t\t(_cyclicBufferPos - delta + _cyclicBufferSize)) << 1;\n\n\t\t\t\tUInt32 pby1 = _bufferOffset + curMatch;\n\t\t\t\tUInt32 len = Math.Min(len0, len1);\n\t\t\t\tif (_bufferBase[pby1 + len] == _bufferBase[cur + len])\n\t\t\t\t{\n\t\t\t\t\twhile(++len != lenLimit)\n\t\t\t\t\t\tif (_bufferBase[pby1 + len] != _bufferBase[cur + len])\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\tif (maxLen < len)\n\t\t\t\t\t{\n\t\t\t\t\t\tdistances[offset++] = maxLen = len;\n\t\t\t\t\t\tdistances[offset++] = delta - 1;\n\t\t\t\t\t\tif (len == lenLimit)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_son[ptr1] = _son[cyclicPos];\n\t\t\t\t\t\t\t_son[ptr0] = _son[cyclicPos + 1];\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (_bufferBase[pby1 + len] < _bufferBase[cur + len])\n\t\t\t\t{\n\t\t\t\t\t_son[ptr1] = curMatch;\n\t\t\t\t\tptr1 = cyclicPos + 1;\n\t\t\t\t\tcurMatch = _son[ptr1];\n\t\t\t\t\tlen1 = len;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t_son[ptr0] = curMatch;\n\t\t\t\t\tptr0 = cyclicPos;\n\t\t\t\t\tcurMatch = _son[ptr0];\n\t\t\t\t\tlen0 = len;\n\t\t\t\t}\n\t\t\t}\n\t\t\tMovePos();\n\t\t\treturn offset;\n\t\t}\n\n\t\tpublic void Skip(UInt32 num)\n\t\t{\n\t\t\tdo\n\t\t\t{\n\t\t\t\tUInt32 lenLimit;\n\t\t\t\tif (_pos + _matchMaxLen <= _streamPos)\n\t\t\t\t\tlenLimit = _matchMaxLen;\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tlenLimit = _streamPos - _pos;\n\t\t\t\t\tif (lenLimit < kMinMatchCheck)\n\t\t\t\t\t{\n\t\t\t\t\t\tMovePos();\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tUInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0;\n\t\t\t\tUInt32 cur = _bufferOffset + _pos;\n\n\t\t\t\tUInt32 hashValue;\n\n\t\t\t\tif (HASH_ARRAY)\n\t\t\t\t{\n\t\t\t\t\tUInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1];\n\t\t\t\t\tUInt32 hash2Value = temp & (kHash2Size - 1);\n\t\t\t\t\t_hash[hash2Value] = _pos;\n\t\t\t\t\ttemp ^= ((UInt32)(_bufferBase[cur + 2]) << 8);\n\t\t\t\t\tUInt32 hash3Value = temp & (kHash3Size - 1);\n\t\t\t\t\t_hash[kHash3Offset + hash3Value] = _pos;\n\t\t\t\t\thashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\thashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8);\n\n\t\t\t\tUInt32 curMatch = _hash[kFixHashSize + hashValue];\n\t\t\t\t_hash[kFixHashSize + hashValue] = _pos;\n\n\t\t\t\tUInt32 ptr0 = (_cyclicBufferPos << 1) + 1;\n\t\t\t\tUInt32 ptr1 = (_cyclicBufferPos << 1);\n\n\t\t\t\tUInt32 len0, len1;\n\t\t\t\tlen0 = len1 = kNumHashDirectBytes;\n\n\t\t\t\tUInt32 count = _cutValue;\n\t\t\t\twhile (true)\n\t\t\t\t{\n\t\t\t\t\tif (curMatch <= matchMinPos || count-- == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\t_son[ptr0] = _son[ptr1] = kEmptyHashValue;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tUInt32 delta = _pos - curMatch;\n\t\t\t\t\tUInt32 cyclicPos = ((delta <= _cyclicBufferPos) ?\n\t\t\t\t\t\t\t\t(_cyclicBufferPos - delta) :\n\t\t\t\t\t\t\t\t(_cyclicBufferPos - delta + _cyclicBufferSize)) << 1;\n\n\t\t\t\t\tUInt32 pby1 = _bufferOffset + curMatch;\n\t\t\t\t\tUInt32 len = Math.Min(len0, len1);\n\t\t\t\t\tif (_bufferBase[pby1 + len] == _bufferBase[cur + len])\n\t\t\t\t\t{\n\t\t\t\t\t\twhile (++len != lenLimit)\n\t\t\t\t\t\t\tif (_bufferBase[pby1 + len] != _bufferBase[cur + len])\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tif (len == lenLimit)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_son[ptr1] = _son[cyclicPos];\n\t\t\t\t\t\t\t_son[ptr0] = _son[cyclicPos + 1];\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (_bufferBase[pby1 + len] < _bufferBase[cur + len])\n\t\t\t\t\t{\n\t\t\t\t\t\t_son[ptr1] = curMatch;\n\t\t\t\t\t\tptr1 = cyclicPos + 1;\n\t\t\t\t\t\tcurMatch = _son[ptr1];\n\t\t\t\t\t\tlen1 = len;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_son[ptr0] = curMatch;\n\t\t\t\t\t\tptr0 = cyclicPos;\n\t\t\t\t\t\tcurMatch = _son[ptr0];\n\t\t\t\t\t\tlen0 = len;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tMovePos();\n\t\t\t}\n\t\t\twhile (--num != 0);\n\t\t}\n\n\t\tvoid NormalizeLinks(UInt32[] items, UInt32 numItems, UInt32 subValue)\n\t\t{\n\t\t\tfor (UInt32 i = 0; i < numItems; i++)\n\t\t\t{\n\t\t\t\tUInt32 value = items[i];\n\t\t\t\tif (value <= subValue)\n\t\t\t\t\tvalue = kEmptyHashValue;\n\t\t\t\telse\n\t\t\t\t\tvalue -= subValue;\n\t\t\t\titems[i] = value;\n\t\t\t}\n\t\t}\n\n\t\tvoid Normalize()\n\t\t{\n\t\t\tUInt32 subValue = _pos - _cyclicBufferSize;\n\t\t\tNormalizeLinks(_son, _cyclicBufferSize * 2, subValue);\n\t\t\tNormalizeLinks(_hash, _hashSizeSum, subValue);\n\t\t\tReduceOffsets((Int32)subValue);\n\t\t}\n\n\t\tpublic void SetCutValue(UInt32 cutValue) { _cutValue = cutValue; }\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZ/LzInWindow.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// LzInWindow.cs\n\nusing System;\n\nnamespace SevenZip.Compression.LZ\n{\n\tpublic class InWindow\n\t{\n\t\tpublic Byte[] _bufferBase = null; // pointer to buffer with data\n\t\tSystem.IO.Stream _stream;\n\t\tUInt32 _posLimit; // offset (from _buffer) of first byte when new block reading must be done\n\t\tbool _streamEndWasReached; // if (true) then _streamPos shows real end of stream\n\n\t\tUInt32 _pointerToLastSafePosition;\n\n\t\tpublic UInt32 _bufferOffset;\n\n\t\tpublic UInt32 _blockSize; // Size of Allocated memory block\n\t\tpublic UInt32 _pos; // offset (from _buffer) of curent byte\n\t\tUInt32 _keepSizeBefore; // how many BYTEs must be kept in buffer before _pos\n\t\tUInt32 _keepSizeAfter; // how many BYTEs must be kept buffer after _pos\n\t\tpublic UInt32 _streamPos; // offset (from _buffer) of first not read byte from Stream\n\n\t\tpublic void MoveBlock()\n\t\t{\n\t\t\tUInt32 offset = (UInt32)(_bufferOffset) + _pos - _keepSizeBefore;\n\t\t\t// we need one additional byte, since MovePos moves on 1 byte.\n\t\t\tif (offset > 0)\n\t\t\t\toffset--;\n\t\t\t\n\t\t\tUInt32 numBytes = (UInt32)(_bufferOffset) + _streamPos - offset;\n\n\t\t\t// check negative offset ????\n\t\t\tfor (UInt32 i = 0; i < numBytes; i++)\n\t\t\t\t_bufferBase[i] = _bufferBase[offset + i];\n\t\t\t_bufferOffset -= offset;\n\t\t}\n\n\t\tpublic virtual void ReadBlock()\n\t\t{\n\t\t\tif (_streamEndWasReached)\n\t\t\t\treturn;\n\t\t\twhile (true)\n\t\t\t{\n\t\t\t\tint size = (int)((0 - _bufferOffset) + _blockSize - _streamPos);\n\t\t\t\tif (size == 0)\n\t\t\t\t\treturn;\n\t\t\t\tint numReadBytes = _stream.Read(_bufferBase, (int)(_bufferOffset + _streamPos), size);\n\t\t\t\tif (numReadBytes == 0)\n\t\t\t\t{\n\t\t\t\t\t_posLimit = _streamPos;\n\t\t\t\t\tUInt32 pointerToPostion = _bufferOffset + _posLimit;\n\t\t\t\t\tif (pointerToPostion > _pointerToLastSafePosition)\n\t\t\t\t\t\t_posLimit = (UInt32)(_pointerToLastSafePosition - _bufferOffset);\n\n\t\t\t\t\t_streamEndWasReached = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_streamPos += (UInt32)numReadBytes;\n\t\t\t\tif (_streamPos >= _pos + _keepSizeAfter)\n\t\t\t\t\t_posLimit = _streamPos - _keepSizeAfter;\n\t\t\t}\n\t\t}\n\n\t\tvoid Free() { _bufferBase = null; }\n\n\t\tpublic void Create(UInt32 keepSizeBefore, UInt32 keepSizeAfter, UInt32 keepSizeReserv)\n\t\t{\n\t\t\t_keepSizeBefore = keepSizeBefore;\n\t\t\t_keepSizeAfter = keepSizeAfter;\n\t\t\tUInt32 blockSize = keepSizeBefore + keepSizeAfter + keepSizeReserv;\n\t\t\tif (_bufferBase == null || _blockSize != blockSize)\n\t\t\t{\n\t\t\t\tFree();\n\t\t\t\t_blockSize = blockSize;\n\t\t\t\t_bufferBase = new Byte[_blockSize];\n\t\t\t}\n\t\t\t_pointerToLastSafePosition = _blockSize - keepSizeAfter;\n\t\t}\n\n\t\tpublic void SetStream(System.IO.Stream stream) { _stream = stream; }\n\t\tpublic void ReleaseStream() { _stream = null; }\n\n\t\tpublic void Init()\n\t\t{\n\t\t\t_bufferOffset = 0;\n\t\t\t_pos = 0;\n\t\t\t_streamPos = 0;\n\t\t\t_streamEndWasReached = false;\n\t\t\tReadBlock();\n\t\t}\n\n\t\tpublic void MovePos()\n\t\t{\n\t\t\t_pos++;\n\t\t\tif (_pos > _posLimit)\n\t\t\t{\n\t\t\t\tUInt32 pointerToPostion = _bufferOffset + _pos;\n\t\t\t\tif (pointerToPostion > _pointerToLastSafePosition)\n\t\t\t\t\tMoveBlock();\n\t\t\t\tReadBlock();\n\t\t\t}\n\t\t}\n\n\t\tpublic Byte GetIndexByte(Int32 index) { return _bufferBase[_bufferOffset + _pos + index]; }\n\n\t\t// index + limit have not to exceed _keepSizeAfter;\n\t\tpublic UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit)\n\t\t{\n\t\t\tif (_streamEndWasReached)\n\t\t\t\tif ((_pos + index) + limit > _streamPos)\n\t\t\t\t\tlimit = _streamPos - (UInt32)(_pos + index);\n\t\t\tdistance++;\n\t\t\t// Byte *pby = _buffer + (size_t)_pos + index;\n\t\t\tUInt32 pby = _bufferOffset + _pos + (UInt32)index;\n\n\t\t\tUInt32 i;\n\t\t\tfor (i = 0; i < limit && _bufferBase[pby + i] == _bufferBase[pby + i - distance]; i++);\n\t\t\treturn i;\n\t\t}\n\n\t\tpublic UInt32 GetNumAvailableBytes() { return _streamPos - _pos; }\n\n\t\tpublic void ReduceOffsets(Int32 subValue)\n\t\t{\n\t\t\t_bufferOffset += (UInt32)subValue;\n\t\t\t_posLimit -= (UInt32)subValue;\n\t\t\t_pos -= (UInt32)subValue;\n\t\t\t_streamPos -= (UInt32)subValue;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZ/LzOutWindow.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591\n\n// LzOutWindow.cs\n\nnamespace SevenZip.Compression.LZ\n{\n\tpublic class OutWindow\n\t{\n\t\tbyte[] _buffer = null;\n\t\tuint _pos;\n\t\tuint _windowSize = 0;\n\t\tuint _streamPos;\n\t\tSystem.IO.Stream _stream;\n\n\t\tpublic uint TrainSize = 0;\n\n\t\tpublic void Create(uint windowSize)\n\t\t{\n\t\t\tif (_windowSize != windowSize)\n\t\t\t{\n\t\t\t\t// System.GC.Collect();\n\t\t\t\t_buffer = new byte[windowSize];\n\t\t\t}\n\t\t\t_windowSize = windowSize;\n\t\t\t_pos = 0;\n\t\t\t_streamPos = 0;\n\t\t}\n\n\t\tpublic void Init(System.IO.Stream stream, bool solid)\n\t\t{\n\t\t\tReleaseStream();\n\t\t\t_stream = stream;\n\t\t\tif (!solid)\n\t\t\t{\n\t\t\t\t_streamPos = 0;\n\t\t\t\t_pos = 0;\n\t\t\t\tTrainSize = 0;\n\t\t\t}\n\t\t}\n\t\n\t\tpublic bool Train(System.IO.Stream stream)\n\t\t{\n\t\t\tlong len = stream.Length;\n\t\t\tuint size = (len < _windowSize) ? (uint)len : _windowSize;\n\t\t\tTrainSize = size;\n\t\t\tstream.Position = len - size;\n\t\t\t_streamPos = _pos = 0;\n\t\t\twhile (size > 0)\n\t\t\t{\n\t\t\t\tuint curSize = _windowSize - _pos;\n\t\t\t\tif (size < curSize)\n\t\t\t\t\tcurSize = size;\n\t\t\t\tint numReadBytes = stream.Read(_buffer, (int)_pos, (int)curSize);\n\t\t\t\tif (numReadBytes == 0)\n\t\t\t\t\treturn false;\n\t\t\t\tsize -= (uint)numReadBytes;\n\t\t\t\t_pos += (uint)numReadBytes;\n\t\t\t\t_streamPos += (uint)numReadBytes;\n\t\t\t\tif (_pos == _windowSize)\n\t\t\t\t\t_streamPos = _pos = 0;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tpublic void ReleaseStream()\n\t\t{\n\t\t\tFlush();\n\t\t\t_stream = null;\n\t\t}\n\n\t\tpublic void Flush()\n\t\t{\n\t\t\tuint size = _pos - _streamPos;\n\t\t\tif (size == 0)\n\t\t\t\treturn;\n\t\t\t_stream.Write(_buffer, (int)_streamPos, (int)size);\n\t\t\tif (_pos >= _windowSize)\n\t\t\t\t_pos = 0;\n\t\t\t_streamPos = _pos;\n\t\t}\n\n\t\tpublic void CopyBlock(uint distance, uint len)\n\t\t{\n\t\t\tuint pos = _pos - distance - 1;\n\t\t\tif (pos >= _windowSize)\n\t\t\t\tpos += _windowSize;\n\t\t\tfor (; len > 0; len--)\n\t\t\t{\n\t\t\t\tif (pos >= _windowSize)\n\t\t\t\t\tpos = 0;\n\t\t\t\t_buffer[_pos++] = _buffer[pos++];\n\t\t\t\tif (_pos >= _windowSize)\n\t\t\t\t\tFlush();\n\t\t\t}\n\t\t}\n\n\t\tpublic void PutByte(byte b)\n\t\t{\n\t\t\t_buffer[_pos++] = b;\n\t\t\tif (_pos >= _windowSize)\n\t\t\t\tFlush();\n\t\t}\n\n\t\tpublic byte GetByte(uint distance)\n\t\t{\n\t\t\tuint pos = _pos - distance - 1;\n\t\t\tif (pos >= _windowSize)\n\t\t\t\tpos += _windowSize;\n\t\t\treturn _buffer[pos];\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZMA/LzmaBase.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\n// LzmaBase.cs\n\nnamespace SevenZip.Compression.LZMA\n{\n\tinternal abstract class Base\n\t{\n\t\tpublic const uint kNumRepDistances = 4;\n\t\tpublic const uint kNumStates = 12;\n\n\t\t// static byte []kLiteralNextStates  = {0, 0, 0, 0, 1, 2, 3, 4,  5,  6,   4, 5};\n\t\t// static byte []kMatchNextStates    = {7, 7, 7, 7, 7, 7, 7, 10, 10, 10, 10, 10};\n\t\t// static byte []kRepNextStates      = {8, 8, 8, 8, 8, 8, 8, 11, 11, 11, 11, 11};\n\t\t// static byte []kShortRepNextStates = {9, 9, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11};\n\n\t\tpublic struct State\n\t\t{\n\t\t\tpublic uint Index;\n\t\t\tpublic void Init() { Index = 0; }\n\t\t\tpublic void UpdateChar()\n\t\t\t{\n\t\t\t\tif (Index < 4) Index = 0;\n\t\t\t\telse if (Index < 10) Index -= 3;\n\t\t\t\telse Index -= 6;\n\t\t\t}\n\t\t\tpublic void UpdateMatch() { Index = (uint)(Index < 7 ? 7 : 10); }\n\t\t\tpublic void UpdateRep() { Index = (uint)(Index < 7 ? 8 : 11); }\n\t\t\tpublic void UpdateShortRep() { Index = (uint)(Index < 7 ? 9 : 11); }\n\t\t\tpublic bool IsCharState() { return Index < 7; }\n\t\t}\n\n\t\tpublic const int kNumPosSlotBits = 6;\n\t\tpublic const int kDicLogSizeMin = 0;\n\t\t// public const int kDicLogSizeMax = 30;\n\t\t// public const uint kDistTableSizeMax = kDicLogSizeMax * 2;\n\n\t\tpublic const int kNumLenToPosStatesBits = 2; // it's for speed optimization\n\t\tpublic const uint kNumLenToPosStates = 1 << kNumLenToPosStatesBits;\n\n\t\tpublic const uint kMatchMinLen = 2;\n\n\t\tpublic static uint GetLenToPosState(uint len)\n\t\t{\n\t\t\tlen -= kMatchMinLen;\n\t\t\tif (len < kNumLenToPosStates)\n\t\t\t\treturn len;\n\t\t\treturn (uint)(kNumLenToPosStates - 1);\n\t\t}\n\n\t\tpublic const int kNumAlignBits = 4;\n\t\tpublic const uint kAlignTableSize = 1 << kNumAlignBits;\n\t\tpublic const uint kAlignMask = (kAlignTableSize - 1);\n\n\t\tpublic const uint kStartPosModelIndex = 4;\n\t\tpublic const uint kEndPosModelIndex = 14;\n\t\tpublic const uint kNumPosModels = kEndPosModelIndex - kStartPosModelIndex;\n\n\t\tpublic const uint kNumFullDistances = 1 << ((int)kEndPosModelIndex / 2);\n\n\t\tpublic const uint kNumLitPosStatesBitsEncodingMax = 4;\n\t\tpublic const uint kNumLitContextBitsMax = 8;\n\n\t\tpublic const int kNumPosStatesBitsMax = 4;\n\t\tpublic const uint kNumPosStatesMax = (1 << kNumPosStatesBitsMax);\n\t\tpublic const int kNumPosStatesBitsEncodingMax = 4;\n\t\tpublic const uint kNumPosStatesEncodingMax = (1 << kNumPosStatesBitsEncodingMax);\n\n\t\tpublic const int kNumLowLenBits = 3;\n\t\tpublic const int kNumMidLenBits = 3;\n\t\tpublic const int kNumHighLenBits = 8;\n\t\tpublic const uint kNumLowLenSymbols = 1 << kNumLowLenBits;\n\t\tpublic const uint kNumMidLenSymbols = 1 << kNumMidLenBits;\n\t\tpublic const uint kNumLenSymbols = kNumLowLenSymbols + kNumMidLenSymbols +\n\t\t\t\t(1 << kNumHighLenBits);\n\t\tpublic const uint kMatchMaxLen = kMatchMinLen + kNumLenSymbols - 1;\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZMA/LzmaDecoder.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591, CS8073\n\n// LzmaDecoder.cs\n\nusing System;\n\nnamespace SevenZip.Compression.LZMA\n{\n    using System.Threading;\n    using RangeCoder;\n\n\tpublic class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream\n\t{\n\t\tclass LenDecoder\n\t\t{\n\t\t\tBitDecoder m_Choice = new BitDecoder();\n\t\t\tBitDecoder m_Choice2 = new BitDecoder();\n\t\t\tBitTreeDecoder[] m_LowCoder = new BitTreeDecoder[Base.kNumPosStatesMax];\n\t\t\tBitTreeDecoder[] m_MidCoder = new BitTreeDecoder[Base.kNumPosStatesMax];\n\t\t\tBitTreeDecoder m_HighCoder = new BitTreeDecoder(Base.kNumHighLenBits);\n\t\t\tuint m_NumPosStates = 0;\n\n\t\t\tpublic void Create(uint numPosStates)\n\t\t\t{\n\t\t\t\tfor (uint posState = m_NumPosStates; posState < numPosStates; posState++)\n\t\t\t\t{\n\t\t\t\t\tm_LowCoder[posState] = new BitTreeDecoder(Base.kNumLowLenBits);\n\t\t\t\t\tm_MidCoder[posState] = new BitTreeDecoder(Base.kNumMidLenBits);\n\t\t\t\t}\n\t\t\t\tm_NumPosStates = numPosStates;\n\t\t\t}\n\n\t\t\tpublic void Init()\n\t\t\t{\n\t\t\t\tm_Choice.Init();\n\t\t\t\tfor (uint posState = 0; posState < m_NumPosStates; posState++)\n\t\t\t\t{\n\t\t\t\t\tm_LowCoder[posState].Init();\n\t\t\t\t\tm_MidCoder[posState].Init();\n\t\t\t\t}\n\t\t\t\tm_Choice2.Init();\n\t\t\t\tm_HighCoder.Init();\n\t\t\t}\n\n\t\t\tpublic uint Decode(RangeCoder.Decoder rangeDecoder, uint posState)\n\t\t\t{\n\t\t\t\tif (m_Choice.Decode(rangeDecoder) == 0)\n\t\t\t\t\treturn m_LowCoder[posState].Decode(rangeDecoder);\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tuint symbol = Base.kNumLowLenSymbols;\n\t\t\t\t\tif (m_Choice2.Decode(rangeDecoder) == 0)\n\t\t\t\t\t\tsymbol += m_MidCoder[posState].Decode(rangeDecoder);\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tsymbol += Base.kNumMidLenSymbols;\n\t\t\t\t\t\tsymbol += m_HighCoder.Decode(rangeDecoder);\n\t\t\t\t\t}\n\t\t\t\t\treturn symbol;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tclass LiteralDecoder\n\t\t{\n\t\t\tstruct Decoder2\n\t\t\t{\n\t\t\t\tBitDecoder[] m_Decoders;\n\t\t\t\tpublic void Create() { m_Decoders = new BitDecoder[0x300]; }\n\t\t\t\tpublic void Init() { for (int i = 0; i < 0x300; i++) m_Decoders[i].Init(); }\n\n\t\t\t\tpublic byte DecodeNormal(RangeCoder.Decoder rangeDecoder)\n\t\t\t\t{\n\t\t\t\t\tuint symbol = 1;\n\t\t\t\t\tdo\n\t\t\t\t\t\tsymbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder);\n\t\t\t\t\twhile (symbol < 0x100);\n\t\t\t\t\treturn (byte)symbol;\n\t\t\t\t}\n\n\t\t\t\tpublic byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, byte matchByte)\n\t\t\t\t{\n\t\t\t\t\tuint symbol = 1;\n\t\t\t\t\tdo\n\t\t\t\t\t{\n\t\t\t\t\t\tuint matchBit = (uint)(matchByte >> 7) & 1;\n\t\t\t\t\t\tmatchByte <<= 1;\n\t\t\t\t\t\tuint bit = m_Decoders[((1 + matchBit) << 8) + symbol].Decode(rangeDecoder);\n\t\t\t\t\t\tsymbol = (symbol << 1) | bit;\n\t\t\t\t\t\tif (matchBit != bit)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twhile (symbol < 0x100)\n\t\t\t\t\t\t\t\tsymbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\twhile (symbol < 0x100);\n\t\t\t\t\treturn (byte)symbol;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tDecoder2[] m_Coders;\n\t\t\tint m_NumPrevBits;\n\t\t\tint m_NumPosBits;\n\t\t\tuint m_PosMask;\n\n\t\t\tpublic void Create(int numPosBits, int numPrevBits)\n\t\t\t{\n\t\t\t\tif (m_Coders != null && m_NumPrevBits == numPrevBits &&\n\t\t\t\t\tm_NumPosBits == numPosBits)\n\t\t\t\t\treturn;\n\t\t\t\tm_NumPosBits = numPosBits;\n\t\t\t\tm_PosMask = ((uint)1 << numPosBits) - 1;\n\t\t\t\tm_NumPrevBits = numPrevBits;\n\t\t\t\tuint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits);\n\t\t\t\tm_Coders = new Decoder2[numStates];\n\t\t\t\tfor (uint i = 0; i < numStates; i++)\n\t\t\t\t\tm_Coders[i].Create();\n\t\t\t}\n\n\t\t\tpublic void Init()\n\t\t\t{\n\t\t\t\tuint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits);\n\t\t\t\tfor (uint i = 0; i < numStates; i++)\n\t\t\t\t\tm_Coders[i].Init();\n\t\t\t}\n\n\t\t\tuint GetState(uint pos, byte prevByte)\n\t\t\t{ return ((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits)); }\n\n\t\t\tpublic byte DecodeNormal(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte)\n\t\t\t{ return m_Coders[GetState(pos, prevByte)].DecodeNormal(rangeDecoder); }\n\n\t\t\tpublic byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte, byte matchByte)\n\t\t\t{ return m_Coders[GetState(pos, prevByte)].DecodeWithMatchByte(rangeDecoder, matchByte); }\n\t\t};\n\n\t\tLZ.OutWindow m_OutWindow = new LZ.OutWindow();\n\t\tRangeCoder.Decoder m_RangeDecoder = new RangeCoder.Decoder();\n\n\t\tBitDecoder[] m_IsMatchDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax];\n\t\tBitDecoder[] m_IsRepDecoders = new BitDecoder[Base.kNumStates];\n\t\tBitDecoder[] m_IsRepG0Decoders = new BitDecoder[Base.kNumStates];\n\t\tBitDecoder[] m_IsRepG1Decoders = new BitDecoder[Base.kNumStates];\n\t\tBitDecoder[] m_IsRepG2Decoders = new BitDecoder[Base.kNumStates];\n\t\tBitDecoder[] m_IsRep0LongDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax];\n\n\t\tBitTreeDecoder[] m_PosSlotDecoder = new BitTreeDecoder[Base.kNumLenToPosStates];\n\t\tBitDecoder[] m_PosDecoders = new BitDecoder[Base.kNumFullDistances - Base.kEndPosModelIndex];\n\n\t\tBitTreeDecoder m_PosAlignDecoder = new BitTreeDecoder(Base.kNumAlignBits);\n\n\t\tLenDecoder m_LenDecoder = new LenDecoder();\n\t\tLenDecoder m_RepLenDecoder = new LenDecoder();\n\n\t\tLiteralDecoder m_LiteralDecoder = new LiteralDecoder();\n\n\t\tuint m_DictionarySize;\n\t\tuint m_DictionarySizeCheck;\n\n\t\tuint m_PosStateMask;\n\n\t\tCancellationToken cancellationToken;\n\n\t\tpublic Decoder()\n\t\t{\n\t\t\tm_DictionarySize = 0xFFFFFFFF;\n\t\t\tfor (int i = 0; i < Base.kNumLenToPosStates; i++)\n\t\t\t\tm_PosSlotDecoder[i] = new BitTreeDecoder(Base.kNumPosSlotBits);\n\t\t}\n\n\t\tpublic Decoder(CancellationToken cancellationToken) : this()\n\t\t{\n\t\t\tthis.cancellationToken = cancellationToken;\n\t\t}\n\n\t\tvoid SetDictionarySize(uint dictionarySize)\n\t\t{\n\t\t\tif (m_DictionarySize != dictionarySize)\n\t\t\t{\n\t\t\t\tm_DictionarySize = dictionarySize;\n\t\t\t\tm_DictionarySizeCheck = Math.Max(m_DictionarySize, 1);\n\t\t\t\tuint blockSize = Math.Max(m_DictionarySizeCheck, (1 << 12));\n\t\t\t\tm_OutWindow.Create(blockSize);\n\t\t\t}\n\t\t}\n\n\t\tvoid SetLiteralProperties(int lp, int lc)\n\t\t{\n\t\t\tif (lp > 8)\n\t\t\t\tthrow new InvalidParamException();\n\t\t\tif (lc > 8)\n\t\t\t\tthrow new InvalidParamException();\n\t\t\tm_LiteralDecoder.Create(lp, lc);\n\t\t}\n\n\t\tvoid SetPosBitsProperties(int pb)\n\t\t{\n\t\t\tif (pb > Base.kNumPosStatesBitsMax)\n\t\t\t\tthrow new InvalidParamException();\n\t\t\tuint numPosStates = (uint)1 << pb;\n\t\t\tm_LenDecoder.Create(numPosStates);\n\t\t\tm_RepLenDecoder.Create(numPosStates);\n\t\t\tm_PosStateMask = numPosStates - 1;\n\t\t}\n\n\t\tbool _solid = false;\n\t\tvoid Init(System.IO.Stream inStream, System.IO.Stream outStream)\n\t\t{\n\t\t\tm_RangeDecoder.Init(inStream);\n\t\t\tm_OutWindow.Init(outStream, _solid);\n\n\t\t\tuint i;\n\t\t\tfor (i = 0; i < Base.kNumStates; i++)\n\t\t\t{\n\t\t\t\tfor (uint j = 0; j <= m_PosStateMask; j++)\n\t\t\t\t{\n\t\t\t\t\tuint index = (i << Base.kNumPosStatesBitsMax) + j;\n\t\t\t\t\tm_IsMatchDecoders[index].Init();\n\t\t\t\t\tm_IsRep0LongDecoders[index].Init();\n\t\t\t\t}\n\t\t\t\tm_IsRepDecoders[i].Init();\n\t\t\t\tm_IsRepG0Decoders[i].Init();\n\t\t\t\tm_IsRepG1Decoders[i].Init();\n\t\t\t\tm_IsRepG2Decoders[i].Init();\n\t\t\t}\n\n\t\t\tm_LiteralDecoder.Init();\n\t\t\tfor (i = 0; i < Base.kNumLenToPosStates; i++)\n\t\t\t\tm_PosSlotDecoder[i].Init();\n\t\t\t// m_PosSpecDecoder.Init();\n\t\t\tfor (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++)\n\t\t\t\tm_PosDecoders[i].Init();\n\n\t\t\tm_LenDecoder.Init();\n\t\t\tm_RepLenDecoder.Init();\n\t\t\tm_PosAlignDecoder.Init();\n\t\t}\n\n\t\tpublic void Code(System.IO.Stream inStream, System.IO.Stream outStream,\n\t\t\tInt64 inSize, Int64 outSize, ICodeProgress progress)\n\t\t{\n\t\t\tInit(inStream, outStream);\n\n\t\t\tBase.State state = new Base.State();\n\t\t\tstate.Init();\n\t\t\tuint rep0 = 0, rep1 = 0, rep2 = 0, rep3 = 0;\n\n\t\t\tUInt64 nowPos64 = 0;\n\t\t\tUInt64 outSize64 = (UInt64)outSize;\n\t\t\tif (nowPos64 < outSize64)\n\t\t\t{\n\t\t\t\tif (m_IsMatchDecoders[state.Index << Base.kNumPosStatesBitsMax].Decode(m_RangeDecoder) != 0)\n\t\t\t\t\tthrow new DataErrorException();\n\t\t\t\tstate.UpdateChar();\n\t\t\t\tbyte b = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, 0, 0);\n\t\t\t\tm_OutWindow.PutByte(b);\n\t\t\t\tnowPos64++;\n\t\t\t}\n\t\t\twhile (nowPos64 < outSize64)\n\t\t\t{\n\t\t\t\tif (cancellationToken != null)\n\t\t\t\t\tcancellationToken.ThrowIfCancellationRequested();\n\n\t\t\t\t// UInt64 next = Math.Min(nowPos64 + (1 << 18), outSize64);\n\t\t\t\t// while(nowPos64 < next)\n\t\t\t\t{\n\t\t\t\t\tuint posState = (uint)nowPos64 & m_PosStateMask;\n\t\t\t\t\tif (m_IsMatchDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tbyte b;\n\t\t\t\t\t\tbyte prevByte = m_OutWindow.GetByte(0);\n\t\t\t\t\t\tif (!state.IsCharState())\n\t\t\t\t\t\t\tb = m_LiteralDecoder.DecodeWithMatchByte(m_RangeDecoder,\n\t\t\t\t\t\t\t\t(uint)nowPos64, prevByte, m_OutWindow.GetByte(rep0));\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tb = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, (uint)nowPos64, prevByte);\n\t\t\t\t\t\tm_OutWindow.PutByte(b);\n\t\t\t\t\t\tstate.UpdateChar();\n\t\t\t\t\t\tnowPos64++;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tuint len;\n\t\t\t\t\t\tif (m_IsRepDecoders[state.Index].Decode(m_RangeDecoder) == 1)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (m_IsRepG0Decoders[state.Index].Decode(m_RangeDecoder) == 0)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif (m_IsRep0LongDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0)\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tstate.UpdateShortRep();\n\t\t\t\t\t\t\t\t\tm_OutWindow.PutByte(m_OutWindow.GetByte(rep0));\n\t\t\t\t\t\t\t\t\tnowPos64++;\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tUInt32 distance;\n\t\t\t\t\t\t\t\tif (m_IsRepG1Decoders[state.Index].Decode(m_RangeDecoder) == 0)\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdistance = rep1;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tif (m_IsRepG2Decoders[state.Index].Decode(m_RangeDecoder) == 0)\n\t\t\t\t\t\t\t\t\t\tdistance = rep2;\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tdistance = rep3;\n\t\t\t\t\t\t\t\t\t\trep3 = rep2;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\trep2 = rep1;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\trep1 = rep0;\n\t\t\t\t\t\t\t\trep0 = distance;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tlen = m_RepLenDecoder.Decode(m_RangeDecoder, posState) + Base.kMatchMinLen;\n\t\t\t\t\t\t\tstate.UpdateRep();\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trep3 = rep2;\n\t\t\t\t\t\t\trep2 = rep1;\n\t\t\t\t\t\t\trep1 = rep0;\n\t\t\t\t\t\t\tlen = Base.kMatchMinLen + m_LenDecoder.Decode(m_RangeDecoder, posState);\n\t\t\t\t\t\t\tstate.UpdateMatch();\n\t\t\t\t\t\t\tuint posSlot = m_PosSlotDecoder[Base.GetLenToPosState(len)].Decode(m_RangeDecoder);\n\t\t\t\t\t\t\tif (posSlot >= Base.kStartPosModelIndex)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tint numDirectBits = (int)((posSlot >> 1) - 1);\n\t\t\t\t\t\t\t\trep0 = ((2 | (posSlot & 1)) << numDirectBits);\n\t\t\t\t\t\t\t\tif (posSlot < Base.kEndPosModelIndex)\n\t\t\t\t\t\t\t\t\trep0 += BitTreeDecoder.ReverseDecode(m_PosDecoders,\n\t\t\t\t\t\t\t\t\t\t\trep0 - posSlot - 1, m_RangeDecoder, numDirectBits);\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\trep0 += (m_RangeDecoder.DecodeDirectBits(\n\t\t\t\t\t\t\t\t\t\tnumDirectBits - Base.kNumAlignBits) << Base.kNumAlignBits);\n\t\t\t\t\t\t\t\t\trep0 += m_PosAlignDecoder.ReverseDecode(m_RangeDecoder);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\trep0 = posSlot;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (rep0 >= m_OutWindow.TrainSize + nowPos64 || rep0 >= m_DictionarySizeCheck)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (rep0 == 0xFFFFFFFF)\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tthrow new DataErrorException();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tm_OutWindow.CopyBlock(rep0, len);\n\t\t\t\t\t\tnowPos64 += len;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tm_OutWindow.Flush();\n\t\t\tm_OutWindow.ReleaseStream();\n\t\t\tm_RangeDecoder.ReleaseStream();\n\t\t}\n\n\t\tpublic void SetDecoderProperties(byte[] properties)\n\t\t{\n\t\t\tif (properties.Length < 5)\n\t\t\t\tthrow new InvalidParamException();\n\t\t\tint lc = properties[0] % 9;\n\t\t\tint remainder = properties[0] / 9;\n\t\t\tint lp = remainder % 5;\n\t\t\tint pb = remainder / 5;\n\t\t\tif (pb > Base.kNumPosStatesBitsMax)\n\t\t\t\tthrow new InvalidParamException();\n\t\t\tUInt32 dictionarySize = 0;\n\t\t\tfor (int i = 0; i < 4; i++)\n\t\t\t\tdictionarySize += ((UInt32)(properties[1 + i])) << (i * 8);\n\t\t\tSetDictionarySize(dictionarySize);\n\t\t\tSetLiteralProperties(lp, lc);\n\t\t\tSetPosBitsProperties(pb);\n\t\t}\n\n\t\tpublic bool Train(System.IO.Stream stream)\n\t\t{\n\t\t\t_solid = true;\n\t\t\treturn m_OutWindow.Train(stream);\n\t\t}\n\n\t\t/*\n\t\tpublic override bool CanRead { get { return true; }}\n\t\tpublic override bool CanWrite { get { return true; }}\n\t\tpublic override bool CanSeek { get { return true; }}\n\t\tpublic override long Length { get { return 0; }}\n\t\tpublic override long Position\n\t\t{\n\t\t\tget { return 0;\t}\n\t\t\tset { }\n\t\t}\n\t\tpublic override void Flush() { }\n\t\tpublic override int Read(byte[] buffer, int offset, int count) \n\t\t{\n\t\t\treturn 0;\n\t\t}\n\t\tpublic override void Write(byte[] buffer, int offset, int count)\n\t\t{\n\t\t}\n\t\tpublic override long Seek(long offset, System.IO.SeekOrigin origin)\n\t\t{\n\t\t\treturn 0;\n\t\t}\n\t\tpublic override void SetLength(long value) {}\n\t\t*/\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/LZMA/LzmaEncoder.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n#pragma warning disable CS1591, CS8073\n\n// LzmaEncoder.cs\n\nusing System;\n\nnamespace SevenZip.Compression.LZMA\n{\n    using System.Threading;\n    using RangeCoder;\n\n\tpublic class Encoder : ICoder, ISetCoderProperties, IWriteCoderProperties\n\t{\n\t\tenum EMatchFinderType\n\t\t{\n\t\t\tBT2,\n\t\t\tBT4,\n\t\t};\n\n\t\tconst UInt32 kIfinityPrice = 0xFFFFFFF;\n\n\t\tstatic Byte[] g_FastPos = new Byte[1 << 11];\n\n\t\tstatic Encoder()\n\t\t{\n\t\t\tconst Byte kFastSlots = 22;\n\t\t\tint c = 2;\n\t\t\tg_FastPos[0] = 0;\n\t\t\tg_FastPos[1] = 1;\n\t\t\tfor (Byte slotFast = 2; slotFast < kFastSlots; slotFast++)\n\t\t\t{\n\t\t\t\tUInt32 k = ((UInt32)1 << ((slotFast >> 1) - 1));\n\t\t\t\tfor (UInt32 j = 0; j < k; j++, c++)\n\t\t\t\t\tg_FastPos[c] = slotFast;\n\t\t\t}\n\t\t}\n\n\t\tstatic UInt32 GetPosSlot(UInt32 pos)\n\t\t{\n\t\t\tif (pos < (1 << 11))\n\t\t\t\treturn g_FastPos[pos];\n\t\t\tif (pos < (1 << 21))\n\t\t\t\treturn (UInt32)(g_FastPos[pos >> 10] + 20);\n\t\t\treturn (UInt32)(g_FastPos[pos >> 20] + 40);\n\t\t}\n\n\t\tstatic UInt32 GetPosSlot2(UInt32 pos)\n\t\t{\n\t\t\tif (pos < (1 << 17))\n\t\t\t\treturn (UInt32)(g_FastPos[pos >> 6] + 12);\n\t\t\tif (pos < (1 << 27))\n\t\t\t\treturn (UInt32)(g_FastPos[pos >> 16] + 32);\n\t\t\treturn (UInt32)(g_FastPos[pos >> 26] + 52);\n\t\t}\n\n\t\tBase.State _state = new Base.State();\n\t\tByte _previousByte;\n\t\tUInt32[] _repDistances = new UInt32[Base.kNumRepDistances];\n\n\t\tvoid BaseInit()\n\t\t{\n\t\t\t_state.Init();\n\t\t\t_previousByte = 0;\n\t\t\tfor (UInt32 i = 0; i < Base.kNumRepDistances; i++)\n\t\t\t\t_repDistances[i] = 0;\n\t\t}\n\n\t\tconst int kDefaultDictionaryLogSize = 22;\n\t\tconst UInt32 kNumFastBytesDefault = 0x20;\n\n\t\tclass LiteralEncoder\n\t\t{\n\t\t\tpublic struct Encoder2\n\t\t\t{\n\t\t\t\tBitEncoder[] m_Encoders;\n\n\t\t\t\tpublic void Create() { m_Encoders = new BitEncoder[0x300]; }\n\n\t\t\t\tpublic void Init() { for (int i = 0; i < 0x300; i++) m_Encoders[i].Init(); }\n\n\t\t\t\tpublic void Encode(RangeCoder.Encoder rangeEncoder, byte symbol)\n\t\t\t\t{\n\t\t\t\t\tuint context = 1;\n\t\t\t\t\tfor (int i = 7; i >= 0; i--)\n\t\t\t\t\t{\n\t\t\t\t\t\tuint bit = (uint)((symbol >> i) & 1);\n\t\t\t\t\t\tm_Encoders[context].Encode(rangeEncoder, bit);\n\t\t\t\t\t\tcontext = (context << 1) | bit;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpublic void EncodeMatched(RangeCoder.Encoder rangeEncoder, byte matchByte, byte symbol)\n\t\t\t\t{\n\t\t\t\t\tuint context = 1;\n\t\t\t\t\tbool same = true;\n\t\t\t\t\tfor (int i = 7; i >= 0; i--)\n\t\t\t\t\t{\n\t\t\t\t\t\tuint bit = (uint)((symbol >> i) & 1);\n\t\t\t\t\t\tuint state = context;\n\t\t\t\t\t\tif (same)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuint matchBit = (uint)((matchByte >> i) & 1);\n\t\t\t\t\t\t\tstate += ((1 + matchBit) << 8);\n\t\t\t\t\t\t\tsame = (matchBit == bit);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tm_Encoders[state].Encode(rangeEncoder, bit);\n\t\t\t\t\t\tcontext = (context << 1) | bit;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpublic uint GetPrice(bool matchMode, byte matchByte, byte symbol)\n\t\t\t\t{\n\t\t\t\t\tuint price = 0;\n\t\t\t\t\tuint context = 1;\n\t\t\t\t\tint i = 7;\n\t\t\t\t\tif (matchMode)\n\t\t\t\t\t{\n\t\t\t\t\t\tfor (; i >= 0; i--)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuint matchBit = (uint)(matchByte >> i) & 1;\n\t\t\t\t\t\t\tuint bit = (uint)(symbol >> i) & 1;\n\t\t\t\t\t\t\tprice += m_Encoders[((1 + matchBit) << 8) + context].GetPrice(bit);\n\t\t\t\t\t\t\tcontext = (context << 1) | bit;\n\t\t\t\t\t\t\tif (matchBit != bit)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ti--;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfor (; i >= 0; i--)\n\t\t\t\t\t{\n\t\t\t\t\t\tuint bit = (uint)(symbol >> i) & 1;\n\t\t\t\t\t\tprice += m_Encoders[context].GetPrice(bit);\n\t\t\t\t\t\tcontext = (context << 1) | bit;\n\t\t\t\t\t}\n\t\t\t\t\treturn price;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tEncoder2[] m_Coders;\n\t\t\tint m_NumPrevBits;\n\t\t\tint m_NumPosBits;\n\t\t\tuint m_PosMask;\n\n\t\t\tpublic void Create(int numPosBits, int numPrevBits)\n\t\t\t{\n\t\t\t\tif (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits)\n\t\t\t\t\treturn;\n\t\t\t\tm_NumPosBits = numPosBits;\n\t\t\t\tm_PosMask = ((uint)1 << numPosBits) - 1;\n\t\t\t\tm_NumPrevBits = numPrevBits;\n\t\t\t\tuint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits);\n\t\t\t\tm_Coders = new Encoder2[numStates];\n\t\t\t\tfor (uint i = 0; i < numStates; i++)\n\t\t\t\t\tm_Coders[i].Create();\n\t\t\t}\n\n\t\t\tpublic void Init()\n\t\t\t{\n\t\t\t\tuint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits);\n\t\t\t\tfor (uint i = 0; i < numStates; i++)\n\t\t\t\t\tm_Coders[i].Init();\n\t\t\t}\n\n\t\t\tpublic Encoder2 GetSubCoder(UInt32 pos, Byte prevByte)\n\t\t\t{ return m_Coders[((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits))]; }\n\t\t}\n\n\t\tclass LenEncoder\n\t\t{\n\t\t\tRangeCoder.BitEncoder _choice = new RangeCoder.BitEncoder();\n\t\t\tRangeCoder.BitEncoder _choice2 = new RangeCoder.BitEncoder();\n\t\t\tRangeCoder.BitTreeEncoder[] _lowCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax];\n\t\t\tRangeCoder.BitTreeEncoder[] _midCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax];\n\t\t\tRangeCoder.BitTreeEncoder _highCoder = new RangeCoder.BitTreeEncoder(Base.kNumHighLenBits);\n\n\t\t\tpublic LenEncoder()\n\t\t\t{\n\t\t\t\tfor (UInt32 posState = 0; posState < Base.kNumPosStatesEncodingMax; posState++)\n\t\t\t\t{\n\t\t\t\t\t_lowCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumLowLenBits);\n\t\t\t\t\t_midCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumMidLenBits);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpublic void Init(UInt32 numPosStates)\n\t\t\t{\n\t\t\t\t_choice.Init();\n\t\t\t\t_choice2.Init();\n\t\t\t\tfor (UInt32 posState = 0; posState < numPosStates; posState++)\n\t\t\t\t{\n\t\t\t\t\t_lowCoder[posState].Init();\n\t\t\t\t\t_midCoder[posState].Init();\n\t\t\t\t}\n\t\t\t\t_highCoder.Init();\n\t\t\t}\n\n\t\t\tpublic void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState)\n\t\t\t{\n\t\t\t\tif (symbol < Base.kNumLowLenSymbols)\n\t\t\t\t{\n\t\t\t\t\t_choice.Encode(rangeEncoder, 0);\n\t\t\t\t\t_lowCoder[posState].Encode(rangeEncoder, symbol);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tsymbol -= Base.kNumLowLenSymbols;\n\t\t\t\t\t_choice.Encode(rangeEncoder, 1);\n\t\t\t\t\tif (symbol < Base.kNumMidLenSymbols)\n\t\t\t\t\t{\n\t\t\t\t\t\t_choice2.Encode(rangeEncoder, 0);\n\t\t\t\t\t\t_midCoder[posState].Encode(rangeEncoder, symbol);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_choice2.Encode(rangeEncoder, 1);\n\t\t\t\t\t\t_highCoder.Encode(rangeEncoder, symbol - Base.kNumMidLenSymbols);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpublic void SetPrices(UInt32 posState, UInt32 numSymbols, UInt32[] prices, UInt32 st)\n\t\t\t{\n\t\t\t\tUInt32 a0 = _choice.GetPrice0();\n\t\t\t\tUInt32 a1 = _choice.GetPrice1();\n\t\t\t\tUInt32 b0 = a1 + _choice2.GetPrice0();\n\t\t\t\tUInt32 b1 = a1 + _choice2.GetPrice1();\n\t\t\t\tUInt32 i = 0;\n\t\t\t\tfor (i = 0; i < Base.kNumLowLenSymbols; i++)\n\t\t\t\t{\n\t\t\t\t\tif (i >= numSymbols)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tprices[st + i] = a0 + _lowCoder[posState].GetPrice(i);\n\t\t\t\t}\n\t\t\t\tfor (; i < Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; i++)\n\t\t\t\t{\n\t\t\t\t\tif (i >= numSymbols)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tprices[st + i] = b0 + _midCoder[posState].GetPrice(i - Base.kNumLowLenSymbols);\n\t\t\t\t}\n\t\t\t\tfor (; i < numSymbols; i++)\n\t\t\t\t\tprices[st + i] = b1 + _highCoder.GetPrice(i - Base.kNumLowLenSymbols - Base.kNumMidLenSymbols);\n\t\t\t}\n\t\t};\n\n\t\tconst UInt32 kNumLenSpecSymbols = Base.kNumLowLenSymbols + Base.kNumMidLenSymbols;\n\n\t\tclass LenPriceTableEncoder : LenEncoder\n\t\t{\n\t\t\tUInt32[] _prices = new UInt32[Base.kNumLenSymbols << Base.kNumPosStatesBitsEncodingMax];\n\t\t\tUInt32 _tableSize;\n\t\t\tUInt32[] _counters = new UInt32[Base.kNumPosStatesEncodingMax];\n\n\t\t\tpublic void SetTableSize(UInt32 tableSize) { _tableSize = tableSize; }\n\n\t\t\tpublic UInt32 GetPrice(UInt32 symbol, UInt32 posState)\n\t\t\t{\n\t\t\t\treturn _prices[posState * Base.kNumLenSymbols + symbol];\n\t\t\t}\n\n\t\t\tvoid UpdateTable(UInt32 posState)\n\t\t\t{\n\t\t\t\tSetPrices(posState, _tableSize, _prices, posState * Base.kNumLenSymbols);\n\t\t\t\t_counters[posState] = _tableSize;\n\t\t\t}\n\n\t\t\tpublic void UpdateTables(UInt32 numPosStates)\n\t\t\t{\n\t\t\t\tfor (UInt32 posState = 0; posState < numPosStates; posState++)\n\t\t\t\t\tUpdateTable(posState);\n\t\t\t}\n\n\t\t\tpublic new void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState)\n\t\t\t{\n\t\t\t\tbase.Encode(rangeEncoder, symbol, posState);\n\t\t\t\tif (--_counters[posState] == 0)\n\t\t\t\t\tUpdateTable(posState);\n\t\t\t}\n\t\t}\n\n\t\tconst UInt32 kNumOpts = 1 << 12;\n\t\tclass Optimal\n\t\t{\n\t\t\tpublic Base.State State;\n\n\t\t\tpublic bool Prev1IsChar;\n\t\t\tpublic bool Prev2;\n\n\t\t\tpublic UInt32 PosPrev2;\n\t\t\tpublic UInt32 BackPrev2;\n\n\t\t\tpublic UInt32 Price;\n\t\t\tpublic UInt32 PosPrev;\n\t\t\tpublic UInt32 BackPrev;\n\n\t\t\tpublic UInt32 Backs0;\n\t\t\tpublic UInt32 Backs1;\n\t\t\tpublic UInt32 Backs2;\n\t\t\tpublic UInt32 Backs3;\n\n\t\t\tpublic void MakeAsChar() { BackPrev = 0xFFFFFFFF; Prev1IsChar = false; }\n\t\t\tpublic void MakeAsShortRep() { BackPrev = 0; ; Prev1IsChar = false; }\n\t\t\tpublic bool IsShortRep() { return (BackPrev == 0); }\n\t\t};\n\t\tOptimal[] _optimum = new Optimal[kNumOpts];\n\t\tLZ.IMatchFinder _matchFinder = null;\n\t\tRangeCoder.Encoder _rangeEncoder = new RangeCoder.Encoder();\n\n\t\tRangeCoder.BitEncoder[] _isMatch = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax];\n\t\tRangeCoder.BitEncoder[] _isRep = new RangeCoder.BitEncoder[Base.kNumStates];\n\t\tRangeCoder.BitEncoder[] _isRepG0 = new RangeCoder.BitEncoder[Base.kNumStates];\n\t\tRangeCoder.BitEncoder[] _isRepG1 = new RangeCoder.BitEncoder[Base.kNumStates];\n\t\tRangeCoder.BitEncoder[] _isRepG2 = new RangeCoder.BitEncoder[Base.kNumStates];\n\t\tRangeCoder.BitEncoder[] _isRep0Long = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax];\n\n\t\tRangeCoder.BitTreeEncoder[] _posSlotEncoder = new RangeCoder.BitTreeEncoder[Base.kNumLenToPosStates];\n\t\t\n\t\tRangeCoder.BitEncoder[] _posEncoders = new RangeCoder.BitEncoder[Base.kNumFullDistances - Base.kEndPosModelIndex];\n\t\tRangeCoder.BitTreeEncoder _posAlignEncoder = new RangeCoder.BitTreeEncoder(Base.kNumAlignBits);\n\n\t\tLenPriceTableEncoder _lenEncoder = new LenPriceTableEncoder();\n\t\tLenPriceTableEncoder _repMatchLenEncoder = new LenPriceTableEncoder();\n\n\t\tLiteralEncoder _literalEncoder = new LiteralEncoder();\n\n\t\tUInt32[] _matchDistances = new UInt32[Base.kMatchMaxLen * 2 + 2];\n\t\t\n\t\tUInt32 _numFastBytes = kNumFastBytesDefault;\n\t\tUInt32 _longestMatchLength;\n\t\tUInt32 _numDistancePairs;\n\n\t\tUInt32 _additionalOffset;\n\n\t\tUInt32 _optimumEndIndex;\n\t\tUInt32 _optimumCurrentIndex;\n\n\t\tbool _longestMatchWasFound;\n\n\t\tUInt32[] _posSlotPrices = new UInt32[1 << (Base.kNumPosSlotBits + Base.kNumLenToPosStatesBits)];\n\t\tUInt32[] _distancesPrices = new UInt32[Base.kNumFullDistances << Base.kNumLenToPosStatesBits];\n\t\tUInt32[] _alignPrices = new UInt32[Base.kAlignTableSize];\n\t\tUInt32 _alignPriceCount;\n\n\t\tUInt32 _distTableSize = (kDefaultDictionaryLogSize * 2);\n\n\t\tint _posStateBits = 2;\n\t\tUInt32 _posStateMask = (4 - 1);\n\t\tint _numLiteralPosStateBits = 0;\n\t\tint _numLiteralContextBits = 3;\n\n\t\tUInt32 _dictionarySize = (1 << kDefaultDictionaryLogSize);\n\t\tUInt32 _dictionarySizePrev = 0xFFFFFFFF;\n\t\tUInt32 _numFastBytesPrev = 0xFFFFFFFF;\n\n\t\tInt64 nowPos64;\n\t\tbool _finished;\n\t\tSystem.IO.Stream _inStream;\n\n\t\tEMatchFinderType _matchFinderType = EMatchFinderType.BT4;\n\t\tbool _writeEndMark = false;\n\t\t\n\t\tbool _needReleaseMFStream;\n\n\t\tCancellationToken cancellationToken;\n\n\t\tvoid Create()\n\t\t{\n\t\t\tif (_matchFinder == null)\n\t\t\t{\n\t\t\t\tLZ.BinTree bt = new LZ.BinTree();\n\t\t\t\tint numHashBytes = 4;\n\t\t\t\tif (_matchFinderType == EMatchFinderType.BT2)\n\t\t\t\t\tnumHashBytes = 2;\n\t\t\t\tbt.SetType(numHashBytes);\n\t\t\t\t_matchFinder = bt;\n\t\t\t}\n\t\t\t_literalEncoder.Create(_numLiteralPosStateBits, _numLiteralContextBits);\n\n\t\t\tif (_dictionarySize == _dictionarySizePrev && _numFastBytesPrev == _numFastBytes)\n\t\t\t\treturn;\n\t\t\t_matchFinder.Create(_dictionarySize, kNumOpts, _numFastBytes, Base.kMatchMaxLen + 1);\n\t\t\t_dictionarySizePrev = _dictionarySize;\n\t\t\t_numFastBytesPrev = _numFastBytes;\n\t\t}\n\n\t\tpublic Encoder()\n\t\t{\n\t\t\tfor (int i = 0; i < kNumOpts; i++)\n\t\t\t\t_optimum[i] = new Optimal();\n\t\t\tfor (int i = 0; i < Base.kNumLenToPosStates; i++)\n\t\t\t\t_posSlotEncoder[i] = new RangeCoder.BitTreeEncoder(Base.kNumPosSlotBits);\n\t\t}\n\n\t\tpublic Encoder(CancellationToken cancellationToken) : this()\n\t\t{\n\t\t\tthis.cancellationToken = cancellationToken;\n\t\t}\n\n\t\tvoid SetWriteEndMarkerMode(bool writeEndMarker)\n\t\t{\n\t\t\t_writeEndMark = writeEndMarker;\n\t\t}\n\n\t\tvoid Init()\n\t\t{\n\t\t\tBaseInit();\n\t\t\t_rangeEncoder.Init();\n\n\t\t\tuint i;\n\t\t\tfor (i = 0; i < Base.kNumStates; i++)\n\t\t\t{\n\t\t\t\tfor (uint j = 0; j <= _posStateMask; j++)\n\t\t\t\t{\n\t\t\t\t\tuint complexState = (i << Base.kNumPosStatesBitsMax) + j;\n\t\t\t\t\t_isMatch[complexState].Init();\n\t\t\t\t\t_isRep0Long[complexState].Init();\n\t\t\t\t}\n\t\t\t\t_isRep[i].Init();\n\t\t\t\t_isRepG0[i].Init();\n\t\t\t\t_isRepG1[i].Init();\n\t\t\t\t_isRepG2[i].Init();\n\t\t\t}\n\t\t\t_literalEncoder.Init();\n\t\t\tfor (i = 0; i < Base.kNumLenToPosStates; i++)\n\t\t\t\t_posSlotEncoder[i].Init();\n\t\t\tfor (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++)\n\t\t\t\t_posEncoders[i].Init();\n\n\t\t\t_lenEncoder.Init((UInt32)1 << _posStateBits);\n\t\t\t_repMatchLenEncoder.Init((UInt32)1 << _posStateBits);\n\n\t\t\t_posAlignEncoder.Init();\n\n\t\t\t_longestMatchWasFound = false;\n\t\t\t_optimumEndIndex = 0;\n\t\t\t_optimumCurrentIndex = 0;\n\t\t\t_additionalOffset = 0;\n\t\t}\n\n\t\tvoid ReadMatchDistances(out UInt32 lenRes, out UInt32 numDistancePairs)\n\t\t{\n\t\t\tlenRes = 0;\n\t\t\tnumDistancePairs = _matchFinder.GetMatches(_matchDistances);\n\t\t\tif (numDistancePairs > 0)\n\t\t\t{\n\t\t\t\tlenRes = _matchDistances[numDistancePairs - 2];\n\t\t\t\tif (lenRes == _numFastBytes)\n\t\t\t\t\tlenRes += _matchFinder.GetMatchLen((int)lenRes - 1, _matchDistances[numDistancePairs - 1],\n\t\t\t\t\t\tBase.kMatchMaxLen - lenRes);\n\t\t\t}\n\t\t\t_additionalOffset++;\n\t\t}\n\n\n\t\tvoid MovePos(UInt32 num)\n\t\t{\n\t\t\tif (num > 0)\n\t\t\t{\n\t\t\t\t_matchFinder.Skip(num);\n\t\t\t\t_additionalOffset += num;\n\t\t\t}\n\t\t}\n\n\t\tUInt32 GetRepLen1Price(Base.State state, UInt32 posState)\n\t\t{\n\t\t\treturn _isRepG0[state.Index].GetPrice0() +\n\t\t\t\t\t_isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0();\n\t\t}\n\n\t\tUInt32 GetPureRepPrice(UInt32 repIndex, Base.State state, UInt32 posState)\n\t\t{\n\t\t\tUInt32 price;\n\t\t\tif (repIndex == 0)\n\t\t\t{\n\t\t\t\tprice = _isRepG0[state.Index].GetPrice0();\n\t\t\t\tprice += _isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1();\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tprice = _isRepG0[state.Index].GetPrice1();\n\t\t\t\tif (repIndex == 1)\n\t\t\t\t\tprice += _isRepG1[state.Index].GetPrice0();\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tprice += _isRepG1[state.Index].GetPrice1();\n\t\t\t\t\tprice += _isRepG2[state.Index].GetPrice(repIndex - 2);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn price;\n\t\t}\n\n\t\tUInt32 GetRepPrice(UInt32 repIndex, UInt32 len, Base.State state, UInt32 posState)\n\t\t{\n\t\t\tUInt32 price = _repMatchLenEncoder.GetPrice(len - Base.kMatchMinLen, posState);\n\t\t\treturn price + GetPureRepPrice(repIndex, state, posState);\n\t\t}\n\t\n\t\tUInt32 GetPosLenPrice(UInt32 pos, UInt32 len, UInt32 posState)\n\t\t{\n\t\t\tUInt32 price;\n\t\t\tUInt32 lenToPosState = Base.GetLenToPosState(len);\n\t\t\tif (pos < Base.kNumFullDistances)\n\t\t\t\tprice = _distancesPrices[(lenToPosState * Base.kNumFullDistances) + pos];\n\t\t\telse\n\t\t\t\tprice = _posSlotPrices[(lenToPosState << Base.kNumPosSlotBits) + GetPosSlot2(pos)] +\n\t\t\t\t\t_alignPrices[pos & Base.kAlignMask];\n\t\t\treturn price + _lenEncoder.GetPrice(len - Base.kMatchMinLen, posState);\n\t\t}\n\n\t\tUInt32 Backward(out UInt32 backRes, UInt32 cur)\n\t\t{\n\t\t\t_optimumEndIndex = cur;\n\t\t\tUInt32 posMem = _optimum[cur].PosPrev;\n\t\t\tUInt32 backMem = _optimum[cur].BackPrev;\n\t\t\tdo\n\t\t\t{\n\t\t\t\tif (_optimum[cur].Prev1IsChar)\n\t\t\t\t{\n\t\t\t\t\t_optimum[posMem].MakeAsChar();\n\t\t\t\t\t_optimum[posMem].PosPrev = posMem - 1;\n\t\t\t\t\tif (_optimum[cur].Prev2)\n\t\t\t\t\t{\n\t\t\t\t\t\t_optimum[posMem - 1].Prev1IsChar = false;\n\t\t\t\t\t\t_optimum[posMem - 1].PosPrev = _optimum[cur].PosPrev2;\n\t\t\t\t\t\t_optimum[posMem - 1].BackPrev = _optimum[cur].BackPrev2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tUInt32 posPrev = posMem;\n\t\t\t\tUInt32 backCur = backMem;\n\n\t\t\t\tbackMem = _optimum[posPrev].BackPrev;\n\t\t\t\tposMem = _optimum[posPrev].PosPrev;\n\n\t\t\t\t_optimum[posPrev].BackPrev = backCur;\n\t\t\t\t_optimum[posPrev].PosPrev = cur;\n\t\t\t\tcur = posPrev;\n\t\t\t}\n\t\t\twhile (cur > 0);\n\t\t\tbackRes = _optimum[0].BackPrev;\n\t\t\t_optimumCurrentIndex = _optimum[0].PosPrev;\n\t\t\treturn _optimumCurrentIndex;\n\t\t}\n\n\t\tUInt32[] reps = new UInt32[Base.kNumRepDistances];\n\t\tUInt32[] repLens = new UInt32[Base.kNumRepDistances];\n\n\n\t\tUInt32 GetOptimum(UInt32 position, out UInt32 backRes)\n\t\t{\n\t\t\tif (_optimumEndIndex != _optimumCurrentIndex)\n\t\t\t{\n\t\t\t\tUInt32 lenRes = _optimum[_optimumCurrentIndex].PosPrev - _optimumCurrentIndex;\n\t\t\t\tbackRes = _optimum[_optimumCurrentIndex].BackPrev;\n\t\t\t\t_optimumCurrentIndex = _optimum[_optimumCurrentIndex].PosPrev;\n\t\t\t\treturn lenRes;\n\t\t\t}\n\t\t\t_optimumCurrentIndex = _optimumEndIndex = 0;\n\n\t\t\tUInt32 lenMain, numDistancePairs;\n\t\t\tif (!_longestMatchWasFound)\n\t\t\t{\n\t\t\t\tReadMatchDistances(out lenMain, out numDistancePairs);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tlenMain = _longestMatchLength;\n\t\t\t\tnumDistancePairs = _numDistancePairs;\n\t\t\t\t_longestMatchWasFound = false;\n\t\t\t}\n\n\t\t\tUInt32 numAvailableBytes = _matchFinder.GetNumAvailableBytes() + 1;\n\t\t\tif (numAvailableBytes < 2)\n\t\t\t{\n\t\t\t\tbackRes = 0xFFFFFFFF;\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t\tif (numAvailableBytes > Base.kMatchMaxLen)\n\t\t\t\tnumAvailableBytes = Base.kMatchMaxLen;\n\n\t\t\tUInt32 repMaxIndex = 0;\n\t\t\tUInt32 i;\t\t\t\n\t\t\tfor (i = 0; i < Base.kNumRepDistances; i++)\n\t\t\t{\n\t\t\t\treps[i] = _repDistances[i];\n\t\t\t\trepLens[i] = _matchFinder.GetMatchLen(0 - 1, reps[i], Base.kMatchMaxLen);\n\t\t\t\tif (repLens[i] > repLens[repMaxIndex])\n\t\t\t\t\trepMaxIndex = i;\n\t\t\t}\n\t\t\tif (repLens[repMaxIndex] >= _numFastBytes)\n\t\t\t{\n\t\t\t\tbackRes = repMaxIndex;\n\t\t\t\tUInt32 lenRes = repLens[repMaxIndex];\n\t\t\t\tMovePos(lenRes - 1);\n\t\t\t\treturn lenRes;\n\t\t\t}\n\n\t\t\tif (lenMain >= _numFastBytes)\n\t\t\t{\n\t\t\t\tbackRes = _matchDistances[numDistancePairs - 1] + Base.kNumRepDistances;\n\t\t\t\tMovePos(lenMain - 1);\n\t\t\t\treturn lenMain;\n\t\t\t}\n\t\t\t\n\t\t\tByte currentByte = _matchFinder.GetIndexByte(0 - 1);\n\t\t\tByte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - 1));\n\n\t\t\tif (lenMain < 2 && currentByte != matchByte && repLens[repMaxIndex] < 2)\n\t\t\t{\n\t\t\t\tbackRes = (UInt32)0xFFFFFFFF;\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t_optimum[0].State = _state;\n\n\t\t\tUInt32 posState = (position & _posStateMask);\n\n\t\t\t_optimum[1].Price = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() +\n\t\t\t\t\t_literalEncoder.GetSubCoder(position, _previousByte).GetPrice(!_state.IsCharState(), matchByte, currentByte);\n\t\t\t_optimum[1].MakeAsChar();\n\n\t\t\tUInt32 matchPrice = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1();\n\t\t\tUInt32 repMatchPrice = matchPrice + _isRep[_state.Index].GetPrice1();\n\n\t\t\tif (matchByte == currentByte)\n\t\t\t{\n\t\t\t\tUInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(_state, posState);\n\t\t\t\tif (shortRepPrice < _optimum[1].Price)\n\t\t\t\t{\n\t\t\t\t\t_optimum[1].Price = shortRepPrice;\n\t\t\t\t\t_optimum[1].MakeAsShortRep();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tUInt32 lenEnd = ((lenMain >= repLens[repMaxIndex]) ? lenMain : repLens[repMaxIndex]);\n\n\t\t\tif(lenEnd < 2)\n\t\t\t{\n\t\t\t\tbackRes = _optimum[1].BackPrev;\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t\t\n\t\t\t_optimum[1].PosPrev = 0;\n\n\t\t\t_optimum[0].Backs0 = reps[0];\n\t\t\t_optimum[0].Backs1 = reps[1];\n\t\t\t_optimum[0].Backs2 = reps[2];\n\t\t\t_optimum[0].Backs3 = reps[3];\n\n\t\t\tUInt32 len = lenEnd;\n\t\t\tdo\n\t\t\t\t_optimum[len--].Price = kIfinityPrice;\n\t\t\twhile (len >= 2);\n\n\t\t\tfor (i = 0; i < Base.kNumRepDistances; i++)\n\t\t\t{\n\t\t\t\tUInt32 repLen = repLens[i];\n\t\t\t\tif (repLen < 2)\n\t\t\t\t\tcontinue;\n\t\t\t\tUInt32 price = repMatchPrice + GetPureRepPrice(i, _state, posState);\n\t\t\t\tdo\n\t\t\t\t{\n\t\t\t\t\tUInt32 curAndLenPrice = price + _repMatchLenEncoder.GetPrice(repLen - 2, posState);\n\t\t\t\t\tOptimal optimum = _optimum[repLen];\n\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t{\n\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\toptimum.PosPrev = 0;\n\t\t\t\t\t\toptimum.BackPrev = i;\n\t\t\t\t\t\toptimum.Prev1IsChar = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twhile (--repLen >= 2);\n\t\t\t}\n\n\t\t\tUInt32 normalMatchPrice = matchPrice + _isRep[_state.Index].GetPrice0();\n\t\t\t\n\t\t\tlen = ((repLens[0] >= 2) ? repLens[0] + 1 : 2);\n\t\t\tif (len <= lenMain)\n\t\t\t{\n\t\t\t\tUInt32 offs = 0;\n\t\t\t\twhile (len > _matchDistances[offs])\n\t\t\t\t\toffs += 2;\n\t\t\t\tfor (; ; len++)\n\t\t\t\t{\n\t\t\t\t\tUInt32 distance = _matchDistances[offs + 1];\n\t\t\t\t\tUInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(distance, len, posState);\n\t\t\t\t\tOptimal optimum = _optimum[len];\n\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t{\n\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\toptimum.PosPrev = 0;\n\t\t\t\t\t\toptimum.BackPrev = distance + Base.kNumRepDistances;\n\t\t\t\t\t\toptimum.Prev1IsChar = false;\n\t\t\t\t\t}\n\t\t\t\t\tif (len == _matchDistances[offs])\n\t\t\t\t\t{\n\t\t\t\t\t\toffs += 2;\n\t\t\t\t\t\tif (offs == numDistancePairs)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tUInt32 cur = 0;\n\n\t\t\twhile (true)\n\t\t\t{\n\t\t\t\tcur++;\n\t\t\t\tif (cur == lenEnd)\n\t\t\t\t\treturn Backward(out backRes, cur);\n\t\t\t\tUInt32 newLen;\n\t\t\t\tReadMatchDistances(out newLen, out numDistancePairs);\n\t\t\t\tif (newLen >= _numFastBytes)\n\t\t\t\t{\n\t\t\t\t\t_numDistancePairs = numDistancePairs;\n\t\t\t\t\t_longestMatchLength = newLen;\n\t\t\t\t\t_longestMatchWasFound = true;\n\t\t\t\t\treturn Backward(out backRes, cur);\n\t\t\t\t}\n\t\t\t\tposition++;\n\t\t\t\tUInt32 posPrev = _optimum[cur].PosPrev;\n\t\t\t\tBase.State state;\n\t\t\t\tif (_optimum[cur].Prev1IsChar)\n\t\t\t\t{\n\t\t\t\t\tposPrev--;\n\t\t\t\t\tif (_optimum[cur].Prev2)\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = _optimum[_optimum[cur].PosPrev2].State;\n\t\t\t\t\t\tif (_optimum[cur].BackPrev2 < Base.kNumRepDistances)\n\t\t\t\t\t\t\tstate.UpdateRep();\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tstate.UpdateMatch();\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\tstate = _optimum[posPrev].State;\n\t\t\t\t\tstate.UpdateChar();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tstate = _optimum[posPrev].State;\n\t\t\t\tif (posPrev == cur - 1)\n\t\t\t\t{\n\t\t\t\t\tif (_optimum[cur].IsShortRep())\n\t\t\t\t\t\tstate.UpdateShortRep();\n\t\t\t\t\telse\n\t\t\t\t\t\tstate.UpdateChar();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tUInt32 pos;\n\t\t\t\t\tif (_optimum[cur].Prev1IsChar && _optimum[cur].Prev2)\n\t\t\t\t\t{\n\t\t\t\t\t\tposPrev = _optimum[cur].PosPrev2;\n\t\t\t\t\t\tpos = _optimum[cur].BackPrev2;\n\t\t\t\t\t\tstate.UpdateRep();\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tpos = _optimum[cur].BackPrev;\n\t\t\t\t\t\tif (pos < Base.kNumRepDistances)\n\t\t\t\t\t\t\tstate.UpdateRep();\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tstate.UpdateMatch();\n\t\t\t\t\t}\n\t\t\t\t\tOptimal opt = _optimum[posPrev];\n\t\t\t\t\tif (pos < Base.kNumRepDistances)\n\t\t\t\t\t{\n\t\t\t\t\t\tif (pos == 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treps[0] = opt.Backs0;\n\t\t\t\t\t\t\treps[1] = opt.Backs1;\n\t\t\t\t\t\t\treps[2] = opt.Backs2;\n\t\t\t\t\t\t\treps[3] = opt.Backs3;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (pos == 1)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treps[0] = opt.Backs1;\n\t\t\t\t\t\t\treps[1] = opt.Backs0;\n\t\t\t\t\t\t\treps[2] = opt.Backs2;\n\t\t\t\t\t\t\treps[3] = opt.Backs3;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (pos == 2)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treps[0] = opt.Backs2;\n\t\t\t\t\t\t\treps[1] = opt.Backs0;\n\t\t\t\t\t\t\treps[2] = opt.Backs1;\n\t\t\t\t\t\t\treps[3] = opt.Backs3;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treps[0] = opt.Backs3;\n\t\t\t\t\t\t\treps[1] = opt.Backs0;\n\t\t\t\t\t\t\treps[2] = opt.Backs1;\n\t\t\t\t\t\t\treps[3] = opt.Backs2;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\treps[0] = (pos - Base.kNumRepDistances);\n\t\t\t\t\t\treps[1] = opt.Backs0;\n\t\t\t\t\t\treps[2] = opt.Backs1;\n\t\t\t\t\t\treps[3] = opt.Backs2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_optimum[cur].State = state;\n\t\t\t\t_optimum[cur].Backs0 = reps[0];\n\t\t\t\t_optimum[cur].Backs1 = reps[1];\n\t\t\t\t_optimum[cur].Backs2 = reps[2];\n\t\t\t\t_optimum[cur].Backs3 = reps[3];\n\t\t\t\tUInt32 curPrice = _optimum[cur].Price;\n\n\t\t\t\tcurrentByte = _matchFinder.GetIndexByte(0 - 1);\n\t\t\t\tmatchByte = _matchFinder.GetIndexByte((Int32)(0 - reps[0] - 1 - 1));\n\n\t\t\t\tposState = (position & _posStateMask);\n\n\t\t\t\tUInt32 curAnd1Price = curPrice +\n\t\t\t\t\t_isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() +\n\t\t\t\t\t_literalEncoder.GetSubCoder(position, _matchFinder.GetIndexByte(0 - 2)).\n\t\t\t\t\tGetPrice(!state.IsCharState(), matchByte, currentByte);\n\n\t\t\t\tOptimal nextOptimum = _optimum[cur + 1];\n\n\t\t\t\tbool nextIsChar = false;\n\t\t\t\tif (curAnd1Price < nextOptimum.Price)\n\t\t\t\t{\n\t\t\t\t\tnextOptimum.Price = curAnd1Price;\n\t\t\t\t\tnextOptimum.PosPrev = cur;\n\t\t\t\t\tnextOptimum.MakeAsChar();\n\t\t\t\t\tnextIsChar = true;\n\t\t\t\t}\n\n\t\t\t\tmatchPrice = curPrice + _isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1();\n\t\t\t\trepMatchPrice = matchPrice + _isRep[state.Index].GetPrice1();\n\n\t\t\t\tif (matchByte == currentByte &&\n\t\t\t\t\t!(nextOptimum.PosPrev < cur && nextOptimum.BackPrev == 0))\n\t\t\t\t{\n\t\t\t\t\tUInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(state, posState);\n\t\t\t\t\tif (shortRepPrice <= nextOptimum.Price)\n\t\t\t\t\t{\n\t\t\t\t\t\tnextOptimum.Price = shortRepPrice;\n\t\t\t\t\t\tnextOptimum.PosPrev = cur;\n\t\t\t\t\t\tnextOptimum.MakeAsShortRep();\n\t\t\t\t\t\tnextIsChar = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tUInt32 numAvailableBytesFull = _matchFinder.GetNumAvailableBytes() + 1;\n\t\t\t\tnumAvailableBytesFull = Math.Min(kNumOpts - 1 - cur, numAvailableBytesFull);\n\t\t\t\tnumAvailableBytes = numAvailableBytesFull;\n\n\t\t\t\tif (numAvailableBytes < 2)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (numAvailableBytes > _numFastBytes)\n\t\t\t\t\tnumAvailableBytes = _numFastBytes;\n\t\t\t\tif (!nextIsChar && matchByte != currentByte)\n\t\t\t\t{\n\t\t\t\t\t// try Literal + rep0\n\t\t\t\t\tUInt32 t = Math.Min(numAvailableBytesFull - 1, _numFastBytes);\n\t\t\t\t\tUInt32 lenTest2 = _matchFinder.GetMatchLen(0, reps[0], t);\n\t\t\t\t\tif (lenTest2 >= 2)\n\t\t\t\t\t{\n\t\t\t\t\t\tBase.State state2 = state;\n\t\t\t\t\t\tstate2.UpdateChar();\n\t\t\t\t\t\tUInt32 posStateNext = (position + 1) & _posStateMask;\n\t\t\t\t\t\tUInt32 nextRepMatchPrice = curAnd1Price +\n\t\t\t\t\t\t\t_isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1() +\n\t\t\t\t\t\t\t_isRep[state2.Index].GetPrice1();\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tUInt32 offset = cur + 1 + lenTest2;\n\t\t\t\t\t\t\twhile (lenEnd < offset)\n\t\t\t\t\t\t\t\t_optimum[++lenEnd].Price = kIfinityPrice;\n\t\t\t\t\t\t\tUInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice(\n\t\t\t\t\t\t\t\t0, lenTest2, state2, posStateNext);\n\t\t\t\t\t\t\tOptimal optimum = _optimum[offset];\n\t\t\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\t\t\toptimum.PosPrev = cur + 1;\n\t\t\t\t\t\t\t\toptimum.BackPrev = 0;\n\t\t\t\t\t\t\t\toptimum.Prev1IsChar = true;\n\t\t\t\t\t\t\t\toptimum.Prev2 = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tUInt32 startLen = 2; // speed optimization \n\n\t\t\t\tfor (UInt32 repIndex = 0; repIndex < Base.kNumRepDistances; repIndex++)\n\t\t\t\t{\n\t\t\t\t\tUInt32 lenTest = _matchFinder.GetMatchLen(0 - 1, reps[repIndex], numAvailableBytes);\n\t\t\t\t\tif (lenTest < 2)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tUInt32 lenTestTemp = lenTest;\n\t\t\t\t\tdo\n\t\t\t\t\t{\n\t\t\t\t\t\twhile (lenEnd < cur + lenTest)\n\t\t\t\t\t\t\t_optimum[++lenEnd].Price = kIfinityPrice;\n\t\t\t\t\t\tUInt32 curAndLenPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState);\n\t\t\t\t\t\tOptimal optimum = _optimum[cur + lenTest];\n\t\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\t\toptimum.PosPrev = cur;\n\t\t\t\t\t\t\toptimum.BackPrev = repIndex;\n\t\t\t\t\t\t\toptimum.Prev1IsChar = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\twhile(--lenTest >= 2);\n\t\t\t\t\tlenTest = lenTestTemp;\n\n\t\t\t\t\tif (repIndex == 0)\n\t\t\t\t\t\tstartLen = lenTest + 1;\n\n\t\t\t\t\t// if (_maxMode)\n\t\t\t\t\tif (lenTest < numAvailableBytesFull)\n\t\t\t\t\t{\n\t\t\t\t\t\tUInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes);\n\t\t\t\t\t\tUInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, reps[repIndex], t);\n\t\t\t\t\t\tif (lenTest2 >= 2)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBase.State state2 = state;\n\t\t\t\t\t\t\tstate2.UpdateRep();\n\t\t\t\t\t\t\tUInt32 posStateNext = (position + lenTest) & _posStateMask;\n\t\t\t\t\t\t\tUInt32 curAndLenCharPrice = \n\t\t\t\t\t\t\t\t\trepMatchPrice + GetRepPrice(repIndex, lenTest, state, posState) + \n\t\t\t\t\t\t\t\t\t_isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() +\n\t\t\t\t\t\t\t\t\t_literalEncoder.GetSubCoder(position + lenTest, \n\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)).GetPrice(true,\n\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)((Int32)lenTest - 1 - (Int32)(reps[repIndex] + 1))), \n\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)lenTest - 1));\n\t\t\t\t\t\t\tstate2.UpdateChar();\n\t\t\t\t\t\t\tposStateNext = (position + lenTest + 1) & _posStateMask;\n\t\t\t\t\t\t\tUInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1();\n\t\t\t\t\t\t\tUInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1();\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// for(; lenTest2 >= 2; lenTest2--)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tUInt32 offset = lenTest + 1 + lenTest2;\n\t\t\t\t\t\t\t\twhile(lenEnd < cur + offset)\n\t\t\t\t\t\t\t\t\t_optimum[++lenEnd].Price = kIfinityPrice;\n\t\t\t\t\t\t\t\tUInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext);\n\t\t\t\t\t\t\t\tOptimal optimum = _optimum[cur + offset];\n\t\t\t\t\t\t\t\tif (curAndLenPrice < optimum.Price) \n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\t\t\t\toptimum.PosPrev = cur + lenTest + 1;\n\t\t\t\t\t\t\t\t\toptimum.BackPrev = 0;\n\t\t\t\t\t\t\t\t\toptimum.Prev1IsChar = true;\n\t\t\t\t\t\t\t\t\toptimum.Prev2 = true;\n\t\t\t\t\t\t\t\t\toptimum.PosPrev2 = cur;\n\t\t\t\t\t\t\t\t\toptimum.BackPrev2 = repIndex;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (newLen > numAvailableBytes)\n\t\t\t\t{\n\t\t\t\t\tnewLen = numAvailableBytes;\n\t\t\t\t\tfor (numDistancePairs = 0; newLen > _matchDistances[numDistancePairs]; numDistancePairs += 2) ;\n\t\t\t\t\t_matchDistances[numDistancePairs] = newLen;\n\t\t\t\t\tnumDistancePairs += 2;\n\t\t\t\t}\n\t\t\t\tif (newLen >= startLen)\n\t\t\t\t{\n\t\t\t\t\tnormalMatchPrice = matchPrice + _isRep[state.Index].GetPrice0();\n\t\t\t\t\twhile (lenEnd < cur + newLen)\n\t\t\t\t\t\t_optimum[++lenEnd].Price = kIfinityPrice;\n\n\t\t\t\t\tUInt32 offs = 0;\n\t\t\t\t\twhile (startLen > _matchDistances[offs])\n\t\t\t\t\t\toffs += 2;\n\n\t\t\t\t\tfor (UInt32 lenTest = startLen; ; lenTest++)\n\t\t\t\t\t{\n\t\t\t\t\t\tUInt32 curBack = _matchDistances[offs + 1];\n\t\t\t\t\t\tUInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(curBack, lenTest, posState);\n\t\t\t\t\t\tOptimal optimum = _optimum[cur + lenTest];\n\t\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\t\toptimum.PosPrev = cur;\n\t\t\t\t\t\t\toptimum.BackPrev = curBack + Base.kNumRepDistances;\n\t\t\t\t\t\t\toptimum.Prev1IsChar = false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (lenTest == _matchDistances[offs])\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif (lenTest < numAvailableBytesFull)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tUInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes);\n\t\t\t\t\t\t\t\tUInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, curBack, t);\n\t\t\t\t\t\t\t\tif (lenTest2 >= 2)\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tBase.State state2 = state;\n\t\t\t\t\t\t\t\t\tstate2.UpdateMatch();\n\t\t\t\t\t\t\t\t\tUInt32 posStateNext = (position + lenTest) & _posStateMask;\n\t\t\t\t\t\t\t\t\tUInt32 curAndLenCharPrice = curAndLenPrice +\n\t\t\t\t\t\t\t\t\t\t_isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() +\n\t\t\t\t\t\t\t\t\t\t_literalEncoder.GetSubCoder(position + lenTest,\n\t\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)).\n\t\t\t\t\t\t\t\t\t\tGetPrice(true,\n\t\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)lenTest - (Int32)(curBack + 1) - 1),\n\t\t\t\t\t\t\t\t\t\t_matchFinder.GetIndexByte((Int32)lenTest - 1));\n\t\t\t\t\t\t\t\t\tstate2.UpdateChar();\n\t\t\t\t\t\t\t\t\tposStateNext = (position + lenTest + 1) & _posStateMask;\n\t\t\t\t\t\t\t\t\tUInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1();\n\t\t\t\t\t\t\t\t\tUInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1();\n\n\t\t\t\t\t\t\t\t\tUInt32 offset = lenTest + 1 + lenTest2;\n\t\t\t\t\t\t\t\t\twhile (lenEnd < cur + offset)\n\t\t\t\t\t\t\t\t\t\t_optimum[++lenEnd].Price = kIfinityPrice;\n\t\t\t\t\t\t\t\t\tcurAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext);\n\t\t\t\t\t\t\t\t\toptimum = _optimum[cur + offset];\n\t\t\t\t\t\t\t\t\tif (curAndLenPrice < optimum.Price)\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\toptimum.Price = curAndLenPrice;\n\t\t\t\t\t\t\t\t\t\toptimum.PosPrev = cur + lenTest + 1;\n\t\t\t\t\t\t\t\t\t\toptimum.BackPrev = 0;\n\t\t\t\t\t\t\t\t\t\toptimum.Prev1IsChar = true;\n\t\t\t\t\t\t\t\t\t\toptimum.Prev2 = true;\n\t\t\t\t\t\t\t\t\t\toptimum.PosPrev2 = cur;\n\t\t\t\t\t\t\t\t\t\toptimum.BackPrev2 = curBack + Base.kNumRepDistances;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\toffs += 2;\n\t\t\t\t\t\t\tif (offs == numDistancePairs)\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tbool ChangePair(UInt32 smallDist, UInt32 bigDist)\n\t\t{\n\t\t\tconst int kDif = 7;\n\t\t\treturn (smallDist < ((UInt32)(1) << (32 - kDif)) && bigDist >= (smallDist << kDif));\n\t\t}\n\n\t\tvoid WriteEndMarker(UInt32 posState)\n\t\t{\n\t\t\tif (!_writeEndMark)\n\t\t\t\treturn;\n\n\t\t\t_isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 1);\n\t\t\t_isRep[_state.Index].Encode(_rangeEncoder, 0);\n\t\t\t_state.UpdateMatch();\n\t\t\tUInt32 len = Base.kMatchMinLen;\n\t\t\t_lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState);\n\t\t\tUInt32 posSlot = (1 << Base.kNumPosSlotBits) - 1;\n\t\t\tUInt32 lenToPosState = Base.GetLenToPosState(len);\n\t\t\t_posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot);\n\t\t\tint footerBits = 30;\n\t\t\tUInt32 posReduced = (((UInt32)1) << footerBits) - 1;\n\t\t\t_rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits);\n\t\t\t_posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask);\n\t\t}\n\n\t\tvoid Flush(UInt32 nowPos)\n\t\t{\n\t\t\tReleaseMFStream();\n\t\t\tWriteEndMarker(nowPos & _posStateMask);\n\t\t\t_rangeEncoder.FlushData();\n\t\t\t_rangeEncoder.FlushStream();\n\t\t}\n\n\t\tpublic void CodeOneBlock(out Int64 inSize, out Int64 outSize, out bool finished)\n\t\t{\n\t\t\tinSize = 0;\n\t\t\toutSize = 0;\n\t\t\tfinished = true;\n\n\t\t\tif (_inStream != null)\n\t\t\t{\n\t\t\t\t_matchFinder.SetStream(_inStream);\n\t\t\t\t_matchFinder.Init();\n\t\t\t\t_needReleaseMFStream = true;\n\t\t\t\t_inStream = null;\n\t\t\t\tif (_trainSize > 0)\n\t\t\t\t\t_matchFinder.Skip(_trainSize);\n\t\t\t}\n\n\t\t\tif (_finished)\n\t\t\t\treturn;\n\t\t\t_finished = true;\n\n\n\t\t\tInt64 progressPosValuePrev = nowPos64;\n\t\t\tif (nowPos64 == 0)\n\t\t\t{\n\t\t\t\tif (_matchFinder.GetNumAvailableBytes() == 0)\n\t\t\t\t{\n\t\t\t\t\tFlush((UInt32)nowPos64);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tUInt32 len, numDistancePairs; // it's not used\n\t\t\t\tReadMatchDistances(out len, out numDistancePairs);\n\t\t\t\tUInt32 posState = (UInt32)(nowPos64) & _posStateMask;\n\t\t\t\t_isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 0);\n\t\t\t\t_state.UpdateChar();\n\t\t\t\tByte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset));\n\t\t\t\t_literalEncoder.GetSubCoder((UInt32)(nowPos64), _previousByte).Encode(_rangeEncoder, curByte);\n\t\t\t\t_previousByte = curByte;\n\t\t\t\t_additionalOffset--;\n\t\t\t\tnowPos64++;\n\t\t\t}\n\t\t\tif (_matchFinder.GetNumAvailableBytes() == 0)\n\t\t\t{\n\t\t\t\tFlush((UInt32)nowPos64);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\twhile (true)\n\t\t\t{\n\t\t\t\tUInt32 pos;\n\t\t\t\tUInt32 len = GetOptimum((UInt32)nowPos64, out pos);\n\t\t\t\t\n\t\t\t\tUInt32 posState = ((UInt32)nowPos64) & _posStateMask;\n\t\t\t\tUInt32 complexState = (_state.Index << Base.kNumPosStatesBitsMax) + posState;\n\t\t\t\tif (len == 1 && pos == 0xFFFFFFFF)\n\t\t\t\t{\n\t\t\t\t\t_isMatch[complexState].Encode(_rangeEncoder, 0);\n\t\t\t\t\tByte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset));\n\t\t\t\t\tLiteralEncoder.Encoder2 subCoder = _literalEncoder.GetSubCoder((UInt32)nowPos64, _previousByte);\n\t\t\t\t\tif (!_state.IsCharState())\n\t\t\t\t\t{\n\t\t\t\t\t\tByte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - _additionalOffset));\n\t\t\t\t\t\tsubCoder.EncodeMatched(_rangeEncoder, matchByte, curByte);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\tsubCoder.Encode(_rangeEncoder, curByte);\n\t\t\t\t\t_previousByte = curByte;\n\t\t\t\t\t_state.UpdateChar();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t_isMatch[complexState].Encode(_rangeEncoder, 1);\n\t\t\t\t\tif (pos < Base.kNumRepDistances)\n\t\t\t\t\t{\n\t\t\t\t\t\t_isRep[_state.Index].Encode(_rangeEncoder, 1);\n\t\t\t\t\t\tif (pos == 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_isRepG0[_state.Index].Encode(_rangeEncoder, 0);\n\t\t\t\t\t\t\tif (len == 1)\n\t\t\t\t\t\t\t\t_isRep0Long[complexState].Encode(_rangeEncoder, 0);\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t_isRep0Long[complexState].Encode(_rangeEncoder, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_isRepG0[_state.Index].Encode(_rangeEncoder, 1);\n\t\t\t\t\t\t\tif (pos == 1)\n\t\t\t\t\t\t\t\t_isRepG1[_state.Index].Encode(_rangeEncoder, 0);\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t_isRepG1[_state.Index].Encode(_rangeEncoder, 1);\n\t\t\t\t\t\t\t\t_isRepG2[_state.Index].Encode(_rangeEncoder, pos - 2);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (len == 1)\n\t\t\t\t\t\t\t_state.UpdateShortRep();\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_repMatchLenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState);\n\t\t\t\t\t\t\t_state.UpdateRep();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tUInt32 distance = _repDistances[pos];\n\t\t\t\t\t\tif (pos != 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfor (UInt32 i = pos; i >= 1; i--)\n\t\t\t\t\t\t\t\t_repDistances[i] = _repDistances[i - 1];\n\t\t\t\t\t\t\t_repDistances[0] = distance;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_isRep[_state.Index].Encode(_rangeEncoder, 0);\n\t\t\t\t\t\t_state.UpdateMatch();\n\t\t\t\t\t\t_lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState);\n\t\t\t\t\t\tpos -= Base.kNumRepDistances;\n\t\t\t\t\t\tUInt32 posSlot = GetPosSlot(pos);\n\t\t\t\t\t\tUInt32 lenToPosState = Base.GetLenToPosState(len);\n\t\t\t\t\t\t_posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot);\n\n\t\t\t\t\t\tif (posSlot >= Base.kStartPosModelIndex)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tint footerBits = (int)((posSlot >> 1) - 1);\n\t\t\t\t\t\t\tUInt32 baseVal = ((2 | (posSlot & 1)) << footerBits);\n\t\t\t\t\t\t\tUInt32 posReduced = pos - baseVal;\n\n\t\t\t\t\t\t\tif (posSlot < Base.kEndPosModelIndex)\n\t\t\t\t\t\t\t\tRangeCoder.BitTreeEncoder.ReverseEncode(_posEncoders,\n\t\t\t\t\t\t\t\t\t\tbaseVal - posSlot - 1, _rangeEncoder, footerBits, posReduced);\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t_rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits);\n\t\t\t\t\t\t\t\t_posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask);\n\t\t\t\t\t\t\t\t_alignPriceCount++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tUInt32 distance = pos;\n\t\t\t\t\t\tfor (UInt32 i = Base.kNumRepDistances - 1; i >= 1; i--)\n\t\t\t\t\t\t\t_repDistances[i] = _repDistances[i - 1];\n\t\t\t\t\t\t_repDistances[0] = distance;\n\t\t\t\t\t\t_matchPriceCount++;\n\t\t\t\t\t}\n\t\t\t\t\t_previousByte = _matchFinder.GetIndexByte((Int32)(len - 1 - _additionalOffset));\n\t\t\t\t}\n\t\t\t\t_additionalOffset -= len;\n\t\t\t\tnowPos64 += len;\n\t\t\t\tif (_additionalOffset == 0)\n\t\t\t\t{\n\t\t\t\t\t// if (!_fastMode)\n\t\t\t\t\tif (_matchPriceCount >= (1 << 7))\n\t\t\t\t\t\tFillDistancesPrices();\n\t\t\t\t\tif (_alignPriceCount >= Base.kAlignTableSize)\n\t\t\t\t\t\tFillAlignPrices();\n\t\t\t\t\tinSize = nowPos64;\n\t\t\t\t\toutSize = _rangeEncoder.GetProcessedSizeAdd();\n\t\t\t\t\tif (_matchFinder.GetNumAvailableBytes() == 0)\n\t\t\t\t\t{\n\t\t\t\t\t\tFlush((UInt32)nowPos64);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (nowPos64 - progressPosValuePrev >= (1 << 12))\n\t\t\t\t\t{\n\t\t\t\t\t\t_finished = false;\n\t\t\t\t\t\tfinished = false;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvoid ReleaseMFStream()\n\t\t{\n\t\t\tif (_matchFinder != null && _needReleaseMFStream)\n\t\t\t{\n\t\t\t\t_matchFinder.ReleaseStream();\n\t\t\t\t_needReleaseMFStream = false;\n\t\t\t}\n\t\t}\n\n\t\tvoid SetOutStream(System.IO.Stream outStream) { _rangeEncoder.SetStream(outStream); }\n\t\tvoid ReleaseOutStream() { _rangeEncoder.ReleaseStream(); }\n\n\t\tvoid ReleaseStreams()\n\t\t{\n\t\t\tReleaseMFStream();\n\t\t\tReleaseOutStream();\n\t\t}\n\n\t\tvoid SetStreams(System.IO.Stream inStream, System.IO.Stream outStream,\n\t\t\t\tInt64 inSize, Int64 outSize)\n\t\t{\n\t\t\t_inStream = inStream;\n\t\t\t_finished = false;\n\t\t\tCreate();\n\t\t\tSetOutStream(outStream);\n\t\t\tInit();\n\n\t\t\t// if (!_fastMode)\n\t\t\t{\n\t\t\t\tFillDistancesPrices();\n\t\t\t\tFillAlignPrices();\n\t\t\t}\n\n\t\t\t_lenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen);\n\t\t\t_lenEncoder.UpdateTables((UInt32)1 << _posStateBits);\n\t\t\t_repMatchLenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen);\n\t\t\t_repMatchLenEncoder.UpdateTables((UInt32)1 << _posStateBits);\n\n\t\t\tnowPos64 = 0;\n\t\t}\n\n\n\t\tpublic void Code(System.IO.Stream inStream, System.IO.Stream outStream,\n\t\t\tInt64 inSize, Int64 outSize, ICodeProgress progress)\n\t\t{\n\t\t\t_needReleaseMFStream = false;\n\t\t\ttry\n\t\t\t{\n\t\t\t\tSetStreams(inStream, outStream, inSize, outSize);\n\t\t\t\twhile (true)\n\t\t\t\t{\n\t\t\t\t\tif (cancellationToken != null)\n\t\t\t\t\t\tcancellationToken.ThrowIfCancellationRequested();\n\n\t\t\t\t\tInt64 processedInSize;\n\t\t\t\t\tInt64 processedOutSize;\n\t\t\t\t\tbool finished;\n\t\t\t\t\tCodeOneBlock(out processedInSize, out processedOutSize, out finished);\n\t\t\t\t\tif (finished)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tif (progress != null)\n\t\t\t\t\t{\n\t\t\t\t\t\tprogress.SetProgress(processedInSize, processedOutSize);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfinally\n\t\t\t{\n\t\t\t\tReleaseStreams();\n\t\t\t}\n\t\t}\n\n\t\tconst int kPropSize = 5;\n\t\tByte[] properties = new Byte[kPropSize];\n\n\t\tpublic void WriteCoderProperties(System.IO.Stream outStream)\n\t\t{\n\t\t\tproperties[0] = (Byte)((_posStateBits * 5 + _numLiteralPosStateBits) * 9 + _numLiteralContextBits);\n\t\t\tfor (int i = 0; i < 4; i++)\n\t\t\t\tproperties[1 + i] = (Byte)((_dictionarySize >> (8 * i)) & 0xFF);\n\t\t\toutStream.Write(properties, 0, kPropSize);\n\t\t}\n\t\t\n\t\tUInt32[] tempPrices = new UInt32[Base.kNumFullDistances];\n\t\tUInt32 _matchPriceCount;\n\n\t\tvoid FillDistancesPrices()\n\t\t{\n\t\t\tfor (UInt32 i = Base.kStartPosModelIndex; i < Base.kNumFullDistances; i++)\n\t\t\t{ \n\t\t\t\tUInt32 posSlot = GetPosSlot(i);\n\t\t\t\tint footerBits = (int)((posSlot >> 1) - 1);\n\t\t\t\tUInt32 baseVal = ((2 | (posSlot & 1)) << footerBits);\n\t\t\t\ttempPrices[i] = BitTreeEncoder.ReverseGetPrice(_posEncoders, \n\t\t\t\t\tbaseVal - posSlot - 1, footerBits, i - baseVal);\n\t\t\t}\n\n\t\t\tfor (UInt32 lenToPosState = 0; lenToPosState < Base.kNumLenToPosStates; lenToPosState++)\n\t\t\t{\n\t\t\t\tUInt32 posSlot;\n\t\t\t\tRangeCoder.BitTreeEncoder encoder = _posSlotEncoder[lenToPosState];\n\t\t\t\n\t\t\t\tUInt32 st = (lenToPosState << Base.kNumPosSlotBits);\n\t\t\t\tfor (posSlot = 0; posSlot < _distTableSize; posSlot++)\n\t\t\t\t\t_posSlotPrices[st + posSlot] = encoder.GetPrice(posSlot);\n\t\t\t\tfor (posSlot = Base.kEndPosModelIndex; posSlot < _distTableSize; posSlot++)\n\t\t\t\t\t_posSlotPrices[st + posSlot] += ((((posSlot >> 1) - 1) - Base.kNumAlignBits) << RangeCoder.BitEncoder.kNumBitPriceShiftBits);\n\n\t\t\t\tUInt32 st2 = lenToPosState * Base.kNumFullDistances;\n\t\t\t\tUInt32 i;\n\t\t\t\tfor (i = 0; i < Base.kStartPosModelIndex; i++)\n\t\t\t\t\t_distancesPrices[st2 + i] = _posSlotPrices[st + i];\n\t\t\t\tfor (; i < Base.kNumFullDistances; i++)\n\t\t\t\t\t_distancesPrices[st2 + i] = _posSlotPrices[st + GetPosSlot(i)] + tempPrices[i];\n\t\t\t}\n\t\t\t_matchPriceCount = 0;\n\t\t}\n\n\t\tvoid FillAlignPrices()\n\t\t{\n\t\t\tfor (UInt32 i = 0; i < Base.kAlignTableSize; i++)\n\t\t\t\t_alignPrices[i] = _posAlignEncoder.ReverseGetPrice(i);\n\t\t\t_alignPriceCount = 0;\n\t\t}\n\n\n\t\tstatic string[] kMatchFinderIDs = \n\t\t{\n\t\t\t\"BT2\",\n\t\t\t\"BT4\",\n\t\t};\n\n\t\tstatic int FindMatchFinder(string s)\n\t\t{\n\t\t\tfor (int m = 0; m < kMatchFinderIDs.Length; m++)\n\t\t\t\tif (s == kMatchFinderIDs[m])\n\t\t\t\t\treturn m;\n\t\t\treturn -1;\n\t\t}\n\t\n\t\tpublic void SetCoderProperties(CoderPropID[] propIDs, object[] properties)\n\t\t{\n\t\t\tfor (UInt32 i = 0; i < properties.Length; i++)\n\t\t\t{\n\t\t\t\tobject prop = properties[i];\n\t\t\t\tswitch (propIDs[i])\n\t\t\t\t{\n\t\t\t\t\tcase CoderPropID.NumFastBytes:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tInt32 numFastBytes = (Int32)prop;\n\t\t\t\t\t\tif (numFastBytes < 5 || numFastBytes > Base.kMatchMaxLen)\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\t_numFastBytes = (UInt32)numFastBytes;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.Algorithm:\n\t\t\t\t\t{\n\t\t\t\t\t\t/*\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tInt32 maximize = (Int32)prop;\n\t\t\t\t\t\t_fastMode = (maximize == 0);\n\t\t\t\t\t\t_maxMode = (maximize >= 2);\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.MatchFinder:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is String))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tEMatchFinderType matchFinderIndexPrev = _matchFinderType;\n\t\t\t\t\t\tint m = FindMatchFinder(((string)prop).ToUpper());\n\t\t\t\t\t\tif (m < 0)\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\t_matchFinderType = (EMatchFinderType)m;\n\t\t\t\t\t\tif (_matchFinder != null && matchFinderIndexPrev != _matchFinderType)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_dictionarySizePrev = 0xFFFFFFFF;\n\t\t\t\t\t\t\t_matchFinder = null;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.DictionarySize:\n\t\t\t\t\t{\n\t\t\t\t\t\tconst int kDicLogSizeMaxCompress = 30;\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException(); ;\n\t\t\t\t\t\tInt32 dictionarySize = (Int32)prop;\n\t\t\t\t\t\tif (dictionarySize < (UInt32)(1 << Base.kDicLogSizeMin) ||\n\t\t\t\t\t\t\tdictionarySize > (UInt32)(1 << kDicLogSizeMaxCompress))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\t_dictionarySize = (UInt32)dictionarySize;\n\t\t\t\t\t\tint dicLogSize;\n\t\t\t\t\t\tfor (dicLogSize = 0; dicLogSize < (UInt32)kDicLogSizeMaxCompress; dicLogSize++)\n\t\t\t\t\t\t\tif (dictionarySize <= ((UInt32)(1) << dicLogSize))\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t_distTableSize = (UInt32)dicLogSize * 2;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.PosStateBits:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tInt32 v = (Int32)prop;\n\t\t\t\t\t\tif (v < 0 || v > (UInt32)Base.kNumPosStatesBitsEncodingMax)\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\t_posStateBits = (int)v;\n\t\t\t\t\t\t_posStateMask = (((UInt32)1) << (int)_posStateBits) - 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.LitPosBits:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tInt32 v = (Int32)prop;\n\t\t\t\t\t\tif (v < 0 || v > (UInt32)Base.kNumLitPosStatesBitsEncodingMax)\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\t_numLiteralPosStateBits = (int)v;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.LitContextBits:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is Int32))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tInt32 v = (Int32)prop;\n\t\t\t\t\t\tif (v < 0 || v > (UInt32)Base.kNumLitContextBitsMax)\n\t\t\t\t\t\t\tthrow new InvalidParamException(); ;\n\t\t\t\t\t\t_numLiteralContextBits = (int)v;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase CoderPropID.EndMarker:\n\t\t\t\t\t{\n\t\t\t\t\t\tif (!(prop is Boolean))\n\t\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t\t\tSetWriteEndMarkerMode((Boolean)prop);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow new InvalidParamException();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tuint _trainSize = 0;\n\t\tpublic void SetTrainSize(uint trainSize)\n\t\t{\n\t\t\t_trainSize = trainSize;\n\t\t}\n\t\t\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/RangeCoder/RangeCoder.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\nusing System;\n\nnamespace SevenZip.Compression.RangeCoder\n{\n\tclass Encoder\n\t{\n\t\tpublic const uint kTopValue = (1 << 24);\n\n\t\tSystem.IO.Stream Stream;\n\n\t\tpublic UInt64 Low;\n\t\tpublic uint Range;\n\t\tuint _cacheSize;\n\t\tbyte _cache;\n\n\t\tlong StartPosition;\n\n\t\tpublic void SetStream(System.IO.Stream stream)\n\t\t{\n\t\t\tStream = stream;\n\t\t}\n\n\t\tpublic void ReleaseStream()\n\t\t{\n\t\t\tStream = null;\n\t\t}\n\n\t\tpublic void Init()\n\t\t{\n\t\t\tStartPosition = Stream.Position;\n\n\t\t\tLow = 0;\n\t\t\tRange = 0xFFFFFFFF;\n\t\t\t_cacheSize = 1;\n\t\t\t_cache = 0;\n\t\t}\n\n\t\tpublic void FlushData()\n\t\t{\n\t\t\tfor (int i = 0; i < 5; i++)\n\t\t\t\tShiftLow();\n\t\t}\n\n\t\tpublic void FlushStream()\n\t\t{\n\t\t\tStream.Flush();\n\t\t}\n\n\t\tpublic void CloseStream()\n\t\t{\n\t\t\tStream.Close();\n\t\t}\n\n\t\tpublic void Encode(uint start, uint size, uint total)\n\t\t{\n\t\t\tLow += start * (Range /= total);\n\t\t\tRange *= size;\n\t\t\twhile (Range < kTopValue)\n\t\t\t{\n\t\t\t\tRange <<= 8;\n\t\t\t\tShiftLow();\n\t\t\t}\n\t\t}\n\n\t\tpublic void ShiftLow()\n\t\t{\n\t\t\tif ((uint)Low < (uint)0xFF000000 || (uint)(Low >> 32) == 1)\n\t\t\t{\n\t\t\t\tbyte temp = _cache;\n\t\t\t\tdo\n\t\t\t\t{\n\t\t\t\t\tStream.WriteByte((byte)(temp + (Low >> 32)));\n\t\t\t\t\ttemp = 0xFF;\n\t\t\t\t}\n\t\t\t\twhile (--_cacheSize != 0);\n\t\t\t\t_cache = (byte)(((uint)Low) >> 24);\n\t\t\t}\n\t\t\t_cacheSize++;\n\t\t\tLow = ((uint)Low) << 8;\n\t\t}\n\n\t\tpublic void EncodeDirectBits(uint v, int numTotalBits)\n\t\t{\n\t\t\tfor (int i = numTotalBits - 1; i >= 0; i--)\n\t\t\t{\n\t\t\t\tRange >>= 1;\n\t\t\t\tif (((v >> i) & 1) == 1)\n\t\t\t\t\tLow += Range;\n\t\t\t\tif (Range < kTopValue)\n\t\t\t\t{\n\t\t\t\t\tRange <<= 8;\n\t\t\t\t\tShiftLow();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tpublic void EncodeBit(uint size0, int numTotalBits, uint symbol)\n\t\t{\n\t\t\tuint newBound = (Range >> numTotalBits) * size0;\n\t\t\tif (symbol == 0)\n\t\t\t\tRange = newBound;\n\t\t\telse\n\t\t\t{\n\t\t\t\tLow += newBound;\n\t\t\t\tRange -= newBound;\n\t\t\t}\n\t\t\twhile (Range < kTopValue)\n\t\t\t{\n\t\t\t\tRange <<= 8;\n\t\t\t\tShiftLow();\n\t\t\t}\n\t\t}\n\n\t\tpublic long GetProcessedSizeAdd()\n\t\t{\n\t\t\treturn _cacheSize +\n\t\t\t\tStream.Position - StartPosition + 4;\n\t\t\t// (long)Stream.GetProcessedSize();\n\t\t}\n\t}\n\n\tclass Decoder\n\t{\n\t\tpublic const uint kTopValue = (1 << 24);\n\t\tpublic uint Range;\n\t\tpublic uint Code;\n\t\t// public Buffer.InBuffer Stream = new Buffer.InBuffer(1 << 16);\n\t\tpublic System.IO.Stream Stream;\n\n\t\tpublic void Init(System.IO.Stream stream)\n\t\t{\n\t\t\t// Stream.Init(stream);\n\t\t\tStream = stream;\n\n\t\t\tCode = 0;\n\t\t\tRange = 0xFFFFFFFF;\n\t\t\tfor (int i = 0; i < 5; i++)\n\t\t\t\tCode = (Code << 8) | (byte)Stream.ReadByte();\n\t\t}\n\n\t\tpublic void ReleaseStream()\n\t\t{\n\t\t\t// Stream.ReleaseStream();\n\t\t\tStream = null;\n\t\t}\n\n\t\tpublic void CloseStream()\n\t\t{\n\t\t\tStream.Close();\n\t\t}\n\n\t\tpublic void Normalize()\n\t\t{\n\t\t\twhile (Range < kTopValue)\n\t\t\t{\n\t\t\t\tCode = (Code << 8) | (byte)Stream.ReadByte();\n\t\t\t\tRange <<= 8;\n\t\t\t}\n\t\t}\n\n\t\tpublic void Normalize2()\n\t\t{\n\t\t\tif (Range < kTopValue)\n\t\t\t{\n\t\t\t\tCode = (Code << 8) | (byte)Stream.ReadByte();\n\t\t\t\tRange <<= 8;\n\t\t\t}\n\t\t}\n\n\t\tpublic uint GetThreshold(uint total)\n\t\t{\n\t\t\treturn Code / (Range /= total);\n\t\t}\n\n\t\tpublic void Decode(uint start, uint size, uint total)\n\t\t{\n\t\t\tCode -= start * Range;\n\t\t\tRange *= size;\n\t\t\tNormalize();\n\t\t}\n\n\t\tpublic uint DecodeDirectBits(int numTotalBits)\n\t\t{\n\t\t\tuint range = Range;\n\t\t\tuint code = Code;\n\t\t\tuint result = 0;\n\t\t\tfor (int i = numTotalBits; i > 0; i--)\n\t\t\t{\n\t\t\t\trange >>= 1;\n\t\t\t\t/*\n\t\t\t\tresult <<= 1;\n\t\t\t\tif (code >= range)\n\t\t\t\t{\n\t\t\t\t\tcode -= range;\n\t\t\t\t\tresult |= 1;\n\t\t\t\t}\n\t\t\t\t*/\n\t\t\t\tuint t = (code - range) >> 31;\n\t\t\t\tcode -= range & (t - 1);\n\t\t\t\tresult = (result << 1) | (1 - t);\n\n\t\t\t\tif (range < kTopValue)\n\t\t\t\t{\n\t\t\t\t\tcode = (code << 8) | (byte)Stream.ReadByte();\n\t\t\t\t\trange <<= 8;\n\t\t\t\t}\n\t\t\t}\n\t\t\tRange = range;\n\t\t\tCode = code;\n\t\t\treturn result;\n\t\t}\n\n\t\tpublic uint DecodeBit(uint size0, int numTotalBits)\n\t\t{\n\t\t\tuint newBound = (Range >> numTotalBits) * size0;\n\t\t\tuint symbol;\n\t\t\tif (Code < newBound)\n\t\t\t{\n\t\t\t\tsymbol = 0;\n\t\t\t\tRange = newBound;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tsymbol = 1;\n\t\t\t\tCode -= newBound;\n\t\t\t\tRange -= newBound;\n\t\t\t}\n\t\t\tNormalize();\n\t\t\treturn symbol;\n\t\t}\n\n\t\t// ulong GetProcessedSize() {return Stream.GetProcessedSize(); }\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/RangeCoder/RangeCoderBit.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\nusing System;\n\nnamespace SevenZip.Compression.RangeCoder\n{\n\tstruct BitEncoder\n\t{\n\t\tpublic const int kNumBitModelTotalBits = 11;\n\t\tpublic const uint kBitModelTotal = (1 << kNumBitModelTotalBits);\n\t\tconst int kNumMoveBits = 5;\n\t\tconst int kNumMoveReducingBits = 2;\n\t\tpublic const int kNumBitPriceShiftBits = 6;\n\n\t\tuint Prob;\n\n\t\tpublic void Init() { Prob = kBitModelTotal >> 1; }\n\n\t\tpublic void UpdateModel(uint symbol)\n\t\t{\n\t\t\tif (symbol == 0)\n\t\t\t\tProb += (kBitModelTotal - Prob) >> kNumMoveBits;\n\t\t\telse\n\t\t\t\tProb -= (Prob) >> kNumMoveBits;\n\t\t}\n\n\t\tpublic void Encode(Encoder encoder, uint symbol)\n\t\t{\n\t\t\t// encoder.EncodeBit(Prob, kNumBitModelTotalBits, symbol);\n\t\t\t// UpdateModel(symbol);\n\t\t\tuint newBound = (encoder.Range >> kNumBitModelTotalBits) * Prob;\n\t\t\tif (symbol == 0)\n\t\t\t{\n\t\t\t\tencoder.Range = newBound;\n\t\t\t\tProb += (kBitModelTotal - Prob) >> kNumMoveBits;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tencoder.Low += newBound;\n\t\t\t\tencoder.Range -= newBound;\n\t\t\t\tProb -= (Prob) >> kNumMoveBits;\n\t\t\t}\n\t\t\tif (encoder.Range < Encoder.kTopValue)\n\t\t\t{\n\t\t\t\tencoder.Range <<= 8;\n\t\t\t\tencoder.ShiftLow();\n\t\t\t}\n\t\t}\n\n\t\tprivate static UInt32[] ProbPrices = new UInt32[kBitModelTotal >> kNumMoveReducingBits];\n\n\t\tstatic BitEncoder()\n\t\t{\n\t\t\tconst int kNumBits = (kNumBitModelTotalBits - kNumMoveReducingBits);\n\t\t\tfor (int i = kNumBits - 1; i >= 0; i--)\n\t\t\t{\n\t\t\t\tUInt32 start = (UInt32)1 << (kNumBits - i - 1);\n\t\t\t\tUInt32 end = (UInt32)1 << (kNumBits - i);\n\t\t\t\tfor (UInt32 j = start; j < end; j++)\n\t\t\t\t\tProbPrices[j] = ((UInt32)i << kNumBitPriceShiftBits) +\n\t\t\t\t\t\t(((end - j) << kNumBitPriceShiftBits) >> (kNumBits - i - 1));\n\t\t\t}\n\t\t}\n\n\t\tpublic uint GetPrice(uint symbol)\n\t\t{\n\t\t\treturn ProbPrices[(((Prob - symbol) ^ ((-(int)symbol))) & (kBitModelTotal - 1)) >> kNumMoveReducingBits];\n\t\t}\n\t  public uint GetPrice0() { return ProbPrices[Prob >> kNumMoveReducingBits]; }\n\t\tpublic uint GetPrice1() { return ProbPrices[(kBitModelTotal - Prob) >> kNumMoveReducingBits]; }\n\t}\n\n\tstruct BitDecoder\n\t{\n\t\tpublic const int kNumBitModelTotalBits = 11;\n\t\tpublic const uint kBitModelTotal = (1 << kNumBitModelTotalBits);\n\t\tconst int kNumMoveBits = 5;\n\n\t\tuint Prob;\n\n\t\tpublic void UpdateModel(int numMoveBits, uint symbol)\n\t\t{\n\t\t\tif (symbol == 0)\n\t\t\t\tProb += (kBitModelTotal - Prob) >> numMoveBits;\n\t\t\telse\n\t\t\t\tProb -= (Prob) >> numMoveBits;\n\t\t}\n\n\t\tpublic void Init() { Prob = kBitModelTotal >> 1; }\n\n\t\tpublic uint Decode(RangeCoder.Decoder rangeDecoder)\n\t\t{\n\t\t\tuint newBound = (uint)(rangeDecoder.Range >> kNumBitModelTotalBits) * (uint)Prob;\n\t\t\tif (rangeDecoder.Code < newBound)\n\t\t\t{\n\t\t\t\trangeDecoder.Range = newBound;\n\t\t\t\tProb += (kBitModelTotal - Prob) >> kNumMoveBits;\n\t\t\t\tif (rangeDecoder.Range < Decoder.kTopValue)\n\t\t\t\t{\n\t\t\t\t\trangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte();\n\t\t\t\t\trangeDecoder.Range <<= 8;\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\trangeDecoder.Range -= newBound;\n\t\t\t\trangeDecoder.Code -= newBound;\n\t\t\t\tProb -= (Prob) >> kNumMoveBits;\n\t\t\t\tif (rangeDecoder.Range < Decoder.kTopValue)\n\t\t\t\t{\n\t\t\t\t\trangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte();\n\t\t\t\t\trangeDecoder.Range <<= 8;\n\t\t\t\t}\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/Compression/RangeCoder/RangeCoderBitTree.cs",
    "content": "// ------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n// ------------------------------------------------------------------------------\n\nusing System;\n\nnamespace SevenZip.Compression.RangeCoder\n{\n\tstruct BitTreeEncoder\n\t{\n\t\tBitEncoder[] Models;\n\t\tint NumBitLevels;\n\n\t\tpublic BitTreeEncoder(int numBitLevels)\n\t\t{\n\t\t\tNumBitLevels = numBitLevels;\n\t\t\tModels = new BitEncoder[1 << numBitLevels];\n\t\t}\n\n\t\tpublic void Init()\n\t\t{\n\t\t\tfor (uint i = 1; i < (1 << NumBitLevels); i++)\n\t\t\t\tModels[i].Init();\n\t\t}\n\n\t\tpublic void Encode(Encoder rangeEncoder, UInt32 symbol)\n\t\t{\n\t\t\tUInt32 m = 1;\n\t\t\tfor (int bitIndex = NumBitLevels; bitIndex > 0; )\n\t\t\t{\n\t\t\t\tbitIndex--;\n\t\t\t\tUInt32 bit = (symbol >> bitIndex) & 1;\n\t\t\t\tModels[m].Encode(rangeEncoder, bit);\n\t\t\t\tm = (m << 1) | bit;\n\t\t\t}\n\t\t}\n\n\t\tpublic void ReverseEncode(Encoder rangeEncoder, UInt32 symbol)\n\t\t{\n\t\t\tUInt32 m = 1;\n\t\t\tfor (UInt32 i = 0; i < NumBitLevels; i++)\n\t\t\t{\n\t\t\t\tUInt32 bit = symbol & 1;\n\t\t\t\tModels[m].Encode(rangeEncoder, bit);\n\t\t\t\tm = (m << 1) | bit;\n\t\t\t\tsymbol >>= 1;\n\t\t\t}\n\t\t}\n\n\t\tpublic UInt32 GetPrice(UInt32 symbol)\n\t\t{\n\t\t\tUInt32 price = 0;\n\t\t\tUInt32 m = 1;\n\t\t\tfor (int bitIndex = NumBitLevels; bitIndex > 0; )\n\t\t\t{\n\t\t\t\tbitIndex--;\n\t\t\t\tUInt32 bit = (symbol >> bitIndex) & 1;\n\t\t\t\tprice += Models[m].GetPrice(bit);\n\t\t\t\tm = (m << 1) + bit;\n\t\t\t}\n\t\t\treturn price;\n\t\t}\n\n\t\tpublic UInt32 ReverseGetPrice(UInt32 symbol)\n\t\t{\n\t\t\tUInt32 price = 0;\n\t\t\tUInt32 m = 1;\n\t\t\tfor (int i = NumBitLevels; i > 0; i--)\n\t\t\t{\n\t\t\t\tUInt32 bit = symbol & 1;\n\t\t\t\tsymbol >>= 1;\n\t\t\t\tprice += Models[m].GetPrice(bit);\n\t\t\t\tm = (m << 1) | bit;\n\t\t\t}\n\t\t\treturn price;\n\t\t}\n\n\t\tpublic static UInt32 ReverseGetPrice(BitEncoder[] Models, UInt32 startIndex,\n\t\t\tint NumBitLevels, UInt32 symbol)\n\t\t{\n\t\t\tUInt32 price = 0;\n\t\t\tUInt32 m = 1;\n\t\t\tfor (int i = NumBitLevels; i > 0; i--)\n\t\t\t{\n\t\t\t\tUInt32 bit = symbol & 1;\n\t\t\t\tsymbol >>= 1;\n\t\t\t\tprice += Models[startIndex + m].GetPrice(bit);\n\t\t\t\tm = (m << 1) | bit;\n\t\t\t}\n\t\t\treturn price;\n\t\t}\n\n\t\tpublic static void ReverseEncode(BitEncoder[] Models, UInt32 startIndex,\n\t\t\tEncoder rangeEncoder, int NumBitLevels, UInt32 symbol)\n\t\t{\n\t\t\tUInt32 m = 1;\n\t\t\tfor (int i = 0; i < NumBitLevels; i++)\n\t\t\t{\n\t\t\t\tUInt32 bit = symbol & 1;\n\t\t\t\tModels[startIndex + m].Encode(rangeEncoder, bit);\n\t\t\t\tm = (m << 1) | bit;\n\t\t\t\tsymbol >>= 1;\n\t\t\t}\n\t\t}\n\t}\n\n\tstruct BitTreeDecoder\n\t{\n\t\tBitDecoder[] Models;\n\t\tint NumBitLevels;\n\n\t\tpublic BitTreeDecoder(int numBitLevels)\n\t\t{\n\t\t\tNumBitLevels = numBitLevels;\n\t\t\tModels = new BitDecoder[1 << numBitLevels];\n\t\t}\n\n\t\tpublic void Init()\n\t\t{\n\t\t\tfor (uint i = 1; i < (1 << NumBitLevels); i++)\n\t\t\t\tModels[i].Init();\n\t\t}\n\n\t\tpublic uint Decode(RangeCoder.Decoder rangeDecoder)\n\t\t{\n\t\t\tuint m = 1;\n\t\t\tfor (int bitIndex = NumBitLevels; bitIndex > 0; bitIndex--)\n\t\t\t\tm = (m << 1) + Models[m].Decode(rangeDecoder);\n\t\t\treturn m - ((uint)1 << NumBitLevels);\n\t\t}\n\n\t\tpublic uint ReverseDecode(RangeCoder.Decoder rangeDecoder)\n\t\t{\n\t\t\tuint m = 1;\n\t\t\tuint symbol = 0;\n\t\t\tfor (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++)\n\t\t\t{\n\t\t\t\tuint bit = Models[m].Decode(rangeDecoder);\n\t\t\t\tm <<= 1;\n\t\t\t\tm += bit;\n\t\t\t\tsymbol |= (bit << bitIndex);\n\t\t\t}\n\t\t\treturn symbol;\n\t\t}\n\n\t\tpublic static uint ReverseDecode(BitDecoder[] Models, UInt32 startIndex,\n\t\t\tRangeCoder.Decoder rangeDecoder, int NumBitLevels)\n\t\t{\n\t\t\tuint m = 1;\n\t\t\tuint symbol = 0;\n\t\t\tfor (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++)\n\t\t\t{\n\t\t\t\tuint bit = Models[startIndex + m].Decode(rangeDecoder);\n\t\t\t\tm <<= 1;\n\t\t\t\tm += bit;\n\t\t\t\tsymbol |= (bit << bitIndex);\n\t\t\t}\n\t\t\treturn symbol;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ClientUpdater/CustomComponent.cs",
    "content": "﻿// Copyright 2022-2025 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater;\n\nusing ClientCore.Extensions;\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Handlers;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nusing ClientUpdater.Compression;\n\nusing Rampastring.Tools;\n\n/// <summary>\n/// Custom component.\n/// </summary>\npublic class CustomComponent\n{\n    /// <summary>\n    /// UI name of custom component.\n    /// </summary>\n    public string GUIName { get; internal set; }\n\n    /// <summary>\n    /// INI name of custom component.\n    /// </summary>\n    public string ININame { get; internal set; }\n\n    /// <summary>\n    /// Local file system path of custom component.\n    /// </summary>\n    public string LocalPath { get; internal set; }\n\n    /// <summary>\n    /// Download path of custom component.\n    /// </summary>\n    public string DownloadPath { get; internal set; }\n\n    /// <summary>\n    /// Is download path treated as an absolute URL?\n    /// </summary>\n    public bool IsDownloadPathAbsolute { get; internal set; }\n\n    /// <summary>\n    /// If set, no archive extension is used for download file path.\n    /// </summary>\n    public bool NoArchiveExtensionForDownloadPath { get; internal set; }\n\n    /// <summary>\n    /// Is this custom component currently being downloaded?\n    /// </summary>\n    public bool IsBeingDownloaded { get; internal set; }\n\n    /// <summary>\n    /// File identifier from local version file.\n    /// </summary>\n    public string LocalIdentifier { get; internal set; }\n\n    /// <summary>\n    /// File identifier from server version file.\n    /// </summary>\n    public string RemoteIdentifier { get; internal set; }\n\n    /// <summary>\n    /// File size from server version file.\n    /// </summary>\n    public long RemoteSize { get; internal set; }\n\n    /// <summary>\n    /// Archive file size from server version file.\n    /// </summary>\n    public long RemoteArchiveSize { get; internal set; }\n\n    /// <summary>\n    /// Is custom component an archived file?\n    /// </summary>\n    public bool Archived { get; internal set; }\n\n    /// <summary>\n    /// Has custom component been initialized?\n    /// </summary>\n    public bool Initialized { get; internal set; }\n\n    private readonly List<string> filesToCleanup = new();\n\n    private int currentDownloadPercentage;\n\n    private CancellationTokenSource downloadTaskCancelTokenSource;\n    private CancellationToken downloadTaskCancelToken;\n\n    /// <summary>\n    /// Creates new custom component.\n    /// </summary>\n    public CustomComponent()\n    {\n    }\n\n    /// <summary>\n    /// Creates new custom component from given information.\n    /// </summary>\n    public CustomComponent(string guiName, string iniName, string downloadPath, string localPath, bool isDownloadPathAbsolute = false, bool noArchiveExtensionForDownloadPath = false)\n    {\n        GUIName = guiName.L10N($\"INI:CustomComponents:{iniName}:UIName\");\n        ININame = iniName;\n        LocalPath = localPath;\n        DownloadPath = downloadPath;\n        IsDownloadPathAbsolute = isDownloadPathAbsolute;\n        NoArchiveExtensionForDownloadPath = noArchiveExtensionForDownloadPath;\n    }\n\n    /// <summary>\n    /// Starts download for this custom component.\n    /// </summary>\n    public void DownloadComponent()\n    {\n        downloadTaskCancelTokenSource ??= new();\n\n        downloadTaskCancelToken = downloadTaskCancelTokenSource.Token;\n\n#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n        DoDownloadComponentAsync(downloadTaskCancelToken);\n#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n    }\n\n    /// <summary>\n    /// Stops downloading of this custom component.\n    /// </summary>\n    public void StopDownload()\n    {\n        if (downloadTaskCancelTokenSource is { IsCancellationRequested: false })\n            downloadTaskCancelTokenSource.Cancel();\n    }\n\n    /// <summary>\n    /// Handles downloading of the custom component.\n    /// </summary>\n    private async Task DoDownloadComponentAsync(CancellationToken cancellationToken)\n    {\n        ProgressMessageHandler progressMessageHandler = null;\n\n        try\n        {\n            Logger.Log(\"CustomComponent: Initializing download of custom component: \" + GUIName);\n\n#if NETFRAMEWORK\n            progressMessageHandler = new(new HttpClientHandler\n            {\n                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,\n                SslProtocols = System.Security.Authentication.SslProtocols.Tls |\n                    System.Security.Authentication.SslProtocols.Tls11 |\n                    System.Security.Authentication.SslProtocols.Tls12 |\n                    System.Security.Authentication.SslProtocols.Tls13,\n            });\n\n            using var httpClient = new HttpClient(progressMessageHandler, true);\n#else\n            progressMessageHandler = new(new SocketsHttpHandler\n            {\n                PooledConnectionLifetime = TimeSpan.FromMinutes(15),\n                AutomaticDecompression = DecompressionMethods.All\n            });\n\n            using var httpClient = new HttpClient(progressMessageHandler, true)\n            {\n                DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher\n            };\n#endif\n\n            IsBeingDownloaded = true;\n            currentDownloadPercentage = -1;\n            string uniqueIdForFile;\n            string uriString = Updater.CurrentUpdateServerURL + Updater.VERSION_FILE;\n            string finalFileName = SafePath.CombineFilePath(Updater.GamePath, LocalPath);\n            string finalFileNameTemp = FormattableString.Invariant($\"{finalFileName}_u\");\n            string versionFileName = SafePath.CombineFilePath(Updater.GamePath, FormattableString.Invariant($\"{Updater.VERSION_FILE}_cc\"));\n            Updater.CreatePath(finalFileName);\n            Updater.UpdateUserAgent(httpClient);\n\n            progressMessageHandler.HttpReceiveProgress += ProgressMessageHandlerOnHttpReceiveProgress;\n\n            Logger.Log(\"CustomComponent: Downloading version info.\");\n\n            var versionFileStream = new FileStream(versionFileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n            using (versionFileStream)\n            {\n                Stream stream = await httpClient.GetStreamAsync(new Uri(uriString)).ConfigureAwait(false);\n\n                using (stream)\n                {\n                    await stream.CopyToAsync(versionFileStream, 81920, cancellationToken).ConfigureAwait(false);\n                }\n            }\n\n            var version = new IniFile(versionFileName);\n            string[] tmp = version.GetStringListValue(\"AddOns\", ININame, string.Empty);\n            Updater.GetArchiveInfo(version, LocalPath, out string archiveID, out int archiveSize);\n            UpdaterFileInfo info = Updater.CreateFileInfo(finalFileName, tmp[0], Conversions.IntFromString(tmp[1], 0), archiveID, archiveSize);\n\n            Logger.Log(\"CustomComponent: Version info parsed. Proceeding to download component.\");\n            int num = 0;\n            Uri downloadUri = GetDownloadUri(DownloadPath, info);\n            string downloadFileName = FormattableString.Invariant($\"{GetArchivePath(finalFileName, info)}_u\");\n            Logger.Log(\"CustomComponent: Download URL for custom component \" + GUIName + \": \" + downloadUri.AbsoluteUri);\n\n            while (true)\n            {\n                filesToCleanup.Clear();\n                filesToCleanup.Add(versionFileName);\n                filesToCleanup.Add(downloadFileName);\n\n                num++;\n\n                var downloadFileStream = new FileStream(downloadFileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n                using (downloadFileStream)\n                {\n                    Stream stream = await httpClient.GetStreamAsync(downloadUri).ConfigureAwait(false);\n\n                    using (stream)\n                    {\n                        await stream.CopyToAsync(downloadFileStream, 81920, cancellationToken).ConfigureAwait(false);\n                    }\n                }\n\n                Logger.Log(\"CustomComponent: Download of custom component \" + GUIName + \" finished - verifying.\");\n\n                if (info.Archived)\n                {\n                    filesToCleanup.Add(finalFileNameTemp);\n                    string archiveLocalPath = GetArchivePath(LocalPath, info);\n                    string archiveLocalPathTemp = FormattableString.Invariant($\"{archiveLocalPath}_u\");\n                    FileInfo archivePathFileInfo = SafePath.GetFile(Updater.GamePath, archiveLocalPathTemp);\n                    Logger.Log(\"CustomComponent: Custom component is an archive.\");\n                    string archiveIdentifier = Updater.GetUniqueIdForFile(archiveLocalPathTemp);\n\n                    if (archiveIdentifier != info.ArchiveIdentifier)\n                    {\n                        cancellationToken.ThrowIfCancellationRequested();\n\n                        if (num > 2)\n                            throw new(\"Too many retries for downloading component.\");\n\n                        Logger.Log(\"CustomComponent: Downloaded archive \" + archiveLocalPath + \"_u has a non-matching identifier: \" + archiveIdentifier + \" against \" + info.ArchiveIdentifier + \". Retrying.\");\n                        Updater.DeleteFileAndWait(archivePathFileInfo.FullName);\n                        continue;\n                    }\n\n                    cancellationToken.ThrowIfCancellationRequested();\n                    Logger.Log(\"CustomComponent: Archive \" + archiveLocalPath + \"_u is intact. Unpacking...\");\n                    await CompressionHelper.DecompressFileAsync(archivePathFileInfo.FullName, finalFileNameTemp, downloadTaskCancelToken).ConfigureAwait(false);\n                    archivePathFileInfo.Delete();\n                }\n\n                cancellationToken.ThrowIfCancellationRequested();\n                uniqueIdForFile = Updater.GetUniqueIdForFile(FormattableString.Invariant($\"{LocalPath}_u\"));\n                if (info.Identifier != uniqueIdForFile)\n                {\n                    if (num > 2)\n                        throw new(\"Too many retries for downloading component.\");\n\n                    cancellationToken.ThrowIfCancellationRequested();\n                    Logger.Log(\"CustomComponent: Incorrect custom component identifier for \" + GUIName + \": \" + uniqueIdForFile + \" against \" + info.Identifier + \". Retrying.\");\n                    continue;\n                }\n\n                break;\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n            Logger.Log(\"Downloaded custom component \" + GUIName + \" verified successfully.\");\n            File.Copy(finalFileNameTemp, finalFileName, true);\n            LocalIdentifier = uniqueIdForFile;\n            IsBeingDownloaded = false;\n            CleanUpAfterDownload();\n            DoDownloadFinished(true);\n        }\n        catch (Exception e)\n        {\n            if (e is AggregateException)\n            {\n                bool canceled = false;\n                bool displayError = false;\n                foreach (Exception ei in (e as AggregateException).InnerExceptions)\n                {\n                    if (ei is TaskCanceledException or OperationCanceledException)\n                    {\n                        canceled = true;\n                    }\n                    else\n                    {\n                        if (!displayError)\n                        {\n                            Logger.Log(\"CustomComponent: One or more errors occurred while downloading custom component \" + GUIName + \". The download has been aborted.\");\n                            displayError = true;\n                        }\n\n                        Logger.Log(\"Message: \" + ei.Message);\n                    }\n\n                    if (canceled)\n                    {\n                        HandleAfterCancelDownload();\n                    }\n                    else\n                    {\n                        IsBeingDownloaded = false;\n                        CleanUpAfterDownload();\n                        DoDownloadFinished(false);\n                    }\n                }\n\n                return;\n            }\n\n            if (e is TaskCanceledException or OperationCanceledException)\n            {\n                HandleAfterCancelDownload();\n                return;\n            }\n\n            Logger.Log(\"CustomComponent: An error occurred while downloading custom component \" + GUIName + \". The download has been aborted. Message: \" + e.Message);\n            IsBeingDownloaded = false;\n            CleanUpAfterDownload();\n            DoDownloadFinished(false);\n        }\n        finally\n        {\n            downloadTaskCancelTokenSource.Dispose();\n            downloadTaskCancelTokenSource = null;\n            progressMessageHandler.HttpReceiveProgress -= ProgressMessageHandlerOnHttpReceiveProgress;\n        }\n    }\n\n    private void HandleAfterCancelDownload()\n    {\n        Logger.Log(\"CustomComponent: Download of custom component \" + GUIName + \" canceled.\");\n        IsBeingDownloaded = false;\n        DoDownloadFinished(false);\n        CleanUpAfterDownload();\n    }\n\n    private Uri GetDownloadUri(string downloadPath, UpdaterFileInfo info)\n    {\n        string fullPath;\n\n        if (!IsDownloadPathAbsolute)\n            fullPath = Updater.CurrentUpdateServerURL + downloadPath;\n        else\n            fullPath = downloadPath;\n\n        return new(NoArchiveExtensionForDownloadPath ? fullPath : GetArchivePath(fullPath, info));\n    }\n\n    private static string GetArchivePath(string path, UpdaterFileInfo info)\n    {\n        if (info.Archived)\n            return path + Updater.ARCHIVE_FILE_EXTENSION;\n\n        return path;\n    }\n\n    private void CleanUpAfterDownload()\n    {\n        try\n        {\n            foreach (string filename in filesToCleanup)\n            {\n                if (File.Exists(filename))\n                {\n                    new FileInfo(filename).IsReadOnly = false;\n                    File.Delete(filename);\n                }\n            }\n        }\n        catch (Exception)\n        {\n        }\n    }\n\n    public event DownloadFinishedEventHandler DownloadFinished;\n\n    public event DownloadProgressChangedEventHandler DownloadProgressChanged;\n\n    public delegate void DownloadFinishedEventHandler(CustomComponent cc, bool success);\n\n    public delegate void DownloadProgressChangedEventHandler(CustomComponent cc, int percentage);\n\n    private void DoDownloadFinished(bool success) => DownloadFinished?.Invoke(this, success);\n\n    private void ProgressMessageHandlerOnHttpReceiveProgress(object sender, HttpProgressEventArgs e)\n    {\n        if (e.ProgressPercentage != currentDownloadPercentage)\n        {\n            currentDownloadPercentage = e.ProgressPercentage;\n            DownloadProgressChanged?.Invoke(this, currentDownloadPercentage);\n        }\n    }\n}"
  },
  {
    "path": "ClientUpdater/UpdateMirror.cs",
    "content": "﻿// Copyright 2022-2024 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater;\n\n/// <summary>\n/// Update mirror info.\n/// </summary>\npublic readonly record struct UpdateMirror(string URL, string Name, string Location);"
  },
  {
    "path": "ClientUpdater/Updater.cs",
    "content": "// Copyright 2022-2025 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater;\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Handlers;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nusing ClientUpdater.Compression;\n\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\npublic static class Updater\n{\n#if NETFRAMEWORK\n    private const string SECOND_STAGE_UPDATER = \"SecondStageUpdater.exe\";\n#else\n    private const string SECOND_STAGE_UPDATER = \"SecondStageUpdater.dll\";\n#endif\n    private const string LEGACY_SECOND_STAGE_UPDATER = \"clientupdt.dat\";\n\n    public const string VERSION_FILE = \"version\";\n    public const string ARCHIVE_FILE_EXTENSION = \".lzma\";\n\n#if NETFRAMEWORK\n    private const string BINARIES_FOLDER = \"Binaries\";\n#else\n    private const string BINARIES_FOLDER = \"BinariesNET8\";\n#endif\n\n    /// <summary>\n    /// Currently set game path for the updater.\n    /// </summary>\n    public static string GamePath { get; private set; } = string.Empty;\n\n    /// <summary>\n    /// Currently set resource path for the updater.\n    /// </summary>\n    public static string ResourcePath { get; private set; } = string.Empty;\n\n    /// <summary>\n    /// Currently set local game ID for the updater.\n    /// </summary>\n    public static string LocalGame { get; private set; } = \"None\";\n\n    /// <summary>\n    /// Currently set calling executable file name for the updater.\n    /// </summary>\n    public static string CallingExecutableFileName { get; private set; } = string.Empty;\n\n    /// <summary>\n    /// Gets read-only collection of all custom components.\n    /// </summary>\n    public static ReadOnlyCollection<CustomComponent> CustomComponents => customComponents?.AsReadOnly();\n\n    /// <summary>\n    /// Gets read-only collection of all update mirrors.\n    /// </summary>\n    public static ReadOnlyCollection<UpdateMirror> UpdateMirrors => updateMirrors?.AsReadOnly();\n\n    /// <summary>\n    /// Update server URL for current update mirror if available.\n    /// </summary>\n    public static string CurrentUpdateServerURL\n        => updateMirrors is { Count: > 0 }\n            ? updateMirrors[currentUpdateMirrorIndex].URL\n            : null;\n\n    private static VersionState _versionState = VersionState.UNKNOWN;\n\n    /// <summary>\n    /// Current version state of the updater.\n    /// </summary>\n    public static VersionState VersionState\n    {\n        get => _versionState;\n\n        private set\n        {\n            _versionState = value;\n            DoOnVersionStateChanged();\n        }\n    }\n\n    /// <summary>\n    /// Does the currently available update (if applicable) require manual download?\n    /// </summary>\n    public static bool ManualUpdateRequired { get; private set; }\n\n    /// <summary>\n    /// Manual download URL for currently available update, if available.\n    /// </summary>\n    public static string ManualDownloadURL { get; private set; } = string.Empty;\n\n    /// <summary>\n    /// Local version file updater version.\n    /// </summary>\n    public static string UpdaterVersion { get; private set; } = \"N/A\";\n\n    /// <summary>\n    /// Local version file game version.\n    /// </summary>\n    public static string GameVersion { get; private set; } = \"N/A\";\n\n    /// <summary>\n    /// Server version file game version.\n    /// </summary>\n    public static string ServerGameVersion { get; private set; } = \"N/A\";\n\n    /// <summary>\n    /// Size of current update in kilobytes.\n    /// </summary>\n    public static int UpdateSizeInKb { get; private set; }\n\n    // Misc.\n    private static int currentUpdateMirrorIndex;\n    private static IniFile settingsINI;\n    private static List<CustomComponent> customComponents;\n    private static List<UpdateMirror> updateMirrors;\n    private static string[] ignoreMasks = [\".rtf\", \".txt\", \"Theme.ini\", \"gui_settings.xml\"];\n\n    // File infos.\n    private static readonly List<UpdaterFileInfo> FileInfosToDownload = new();\n    private static readonly List<UpdaterFileInfo> ServerFileInfos = new();\n    private static readonly List<UpdaterFileInfo> LocalFileInfos = new();\n\n#if NETFRAMEWORK\n    private static readonly Lazy<ProgressMessageHandler> lazySharedProgressMessageHandler = new Lazy<ProgressMessageHandler>(() =>\n        new(new HttpClientHandler\n        {\n            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,\n            SslProtocols = System.Security.Authentication.SslProtocols.Tls |\n                System.Security.Authentication.SslProtocols.Tls11 |\n                System.Security.Authentication.SslProtocols.Tls12 |\n                System.Security.Authentication.SslProtocols.Tls13,\n        }), LazyThreadSafetyMode.ExecutionAndPublication);\n\n    private static readonly Lazy<HttpClient> lazySharedHttpClient = new Lazy<HttpClient>(() =>\n        new(SharedProgressMessageHandler, true), LazyThreadSafetyMode.ExecutionAndPublication);\n#else\n    private static readonly Lazy<ProgressMessageHandler> lazySharedProgressMessageHandler = new Lazy<ProgressMessageHandler>(() =>\n        new(new SocketsHttpHandler\n        {\n            PooledConnectionLifetime = TimeSpan.FromMinutes(15),\n            AutomaticDecompression = DecompressionMethods.All\n        }), LazyThreadSafetyMode.ExecutionAndPublication);\n\n    private static readonly Lazy<HttpClient> lazySharedHttpClient = new Lazy<HttpClient>(() =>\n      new HttpClient(SharedProgressMessageHandler, true)\n      {\n          DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher\n      }, LazyThreadSafetyMode.ExecutionAndPublication);\n#endif\n\n    private static readonly ProgressMessageHandler SharedProgressMessageHandler = lazySharedProgressMessageHandler.Value;\n\n    private static readonly HttpClient SharedHttpClient = lazySharedHttpClient.Value;\n\n    // Current update / download related.\n    private static bool terminateUpdate;\n    private static string currentFilename;\n    private static int currentFileSize;\n    private static int totalDownloadedKbs;\n\n    /// <summary>\n    /// Initializes the updater.\n    /// </summary>\n    /// <param name=\"gamePath\">Path of the root client / game folder.</param>\n    /// <param name=\"resourcePath\">Path of the resource folder of client / game.</param>\n    /// <param name=\"settingsIniName\">Client settings INI filename.</param>\n    /// <param name=\"localGame\">Local game ID of the current game.</param>\n    /// <param name=\"callingExecutableFileName\">File name of the calling executable.</param>\n    public static void Initialize(string gamePath, string resourcePath, string settingsIniName, string localGame, string callingExecutableFileName)\n    {\n        Logger.Log(\"Updater: Initializing updater.\");\n\n        GamePath = gamePath;\n        ResourcePath = resourcePath;\n        settingsINI = new(SafePath.CombineFilePath(GamePath, settingsIniName));\n        LocalGame = localGame;\n        CallingExecutableFileName = callingExecutableFileName;\n\n        ReadUpdaterConfig();\n\n        Logger.Log(\"Updater: Update mirror count: \" + updateMirrors.Count);\n        Logger.Log(\"Updater: Running from: \" + CallingExecutableFileName);\n        var list = new List<UpdateMirror>();\n        List<string> sectionKeys = settingsINI.GetSectionKeys(\"DownloadMirrors\");\n\n        if (sectionKeys != null)\n        {\n            foreach (string str in sectionKeys)\n            {\n                string value = settingsINI.GetStringValue(\"DownloadMirrors\", str, string.Empty);\n\n                if (updateMirrors.Any(um => value.Equals(um.Name, StringComparison.OrdinalIgnoreCase)))\n                {\n                    UpdateMirror item = updateMirrors.Single(um => value.Equals(um.Name, StringComparison.OrdinalIgnoreCase));\n\n                    if (!list.Contains(item))\n                        list.Add(item);\n                }\n            }\n        }\n\n        foreach (UpdateMirror mirror2 in updateMirrors)\n        {\n            if (!list.Contains(mirror2))\n                list.Add(mirror2);\n        }\n\n        updateMirrors = list;\n    }\n\n    /// <summary>\n    /// Checks if there are available updates.\n    /// </summary>\n    public static void CheckForUpdates()\n    {\n        Logger.Log(\"Updater: Checking for updates.\");\n        if (VersionState is not VersionState.UPDATECHECKINPROGRESS and not VersionState.UPDATEINPROGRESS)\n#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n            DoVersionCheckAsync();\n#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n    }\n\n    /// <summary>\n    /// Checks version information of local files.\n    /// </summary>\n    public static void CheckLocalFileVersions()\n    {\n        Logger.Log(\"Updater: Checking local file versions.\");\n\n        LocalFileInfos.Clear();\n\n        var file = new IniFile(SafePath.CombineFilePath(GamePath, VERSION_FILE));\n        GameVersion = file.GetStringValue(\"DTA\", \"Version\", \"N/A\");\n        UpdaterVersion = file.GetStringValue(\"DTA\", \"UpdaterVersion\", \"N/A\");\n        List<string> sectionKeys = file.GetSectionKeys(\"FileVersions\");\n\n        if (sectionKeys != null)\n        {\n            char[] separator = new char[] { ',' };\n            foreach (string str in sectionKeys)\n            {\n                string[] strArray = file.GetStringListValue(\"FileVersions\", str, string.Empty, separator);\n                string[] strArrayArch = file.GetStringListValue(\"ArchivedFiles\", str, string.Empty, separator);\n                bool archiveAvailable = strArrayArch is { Length: >= 2 };\n\n                if (strArray.Length >= 2)\n                {\n                    var item = new UpdaterFileInfo(\n                        SafePath.CombineFilePath(str), Conversions.IntFromString(strArray[1], 0))\n                    {\n                        Identifier = strArray[0],\n                        ArchiveIdentifier = archiveAvailable ? strArrayArch[0] : string.Empty,\n                        ArchiveSize = archiveAvailable ? Conversions.IntFromString(strArrayArch[1], 0) : 0\n                    };\n\n                    LocalFileInfos.Add(item);\n                }\n                else\n                {\n                    Logger.Log(\"Updater: Warning: Malformed file info in local version information: \" + str);\n                }\n            }\n        }\n\n        OnLocalFileVersionsChecked?.Invoke();\n    }\n\n    /// <summary>\n    /// Starts update process.\n    /// </summary>\n#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n    public static void StartUpdate() => PerformUpdateAsync();\n#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n\n    /// <summary>\n    /// Stops current update process.\n    /// </summary>\n    public static void StopUpdate() => terminateUpdate = true;\n\n    /// <summary>\n    /// Clears current version file information.\n    /// </summary>\n    public static void ClearVersionInfo()\n    {\n        LocalFileInfos.Clear();\n        ServerFileInfos.Clear();\n        FileInfosToDownload.Clear();\n        GameVersion = \"N/A\";\n        VersionState = VersionState.UNKNOWN;\n    }\n\n    /// <summary>\n    /// Checks if file\n    /// </summary>\n    public static bool IsFileNonexistantOrOriginal(string filePath)\n    {\n        UpdaterFileInfo info = LocalFileInfos.Find(f => f.Filename.Equals(filePath, StringComparison.OrdinalIgnoreCase));\n\n        if (info == null)\n            return true;\n\n        string uniqueIdForFile = GetUniqueIdForFile(info.Filename);\n        return info.Identifier == uniqueIdForFile;\n    }\n\n    /// <summary>\n    /// Moves update mirror down in list of update mirrors.\n    /// </summary>\n    /// <param name=\"mirrorIndex\">Index of mirror to move in the list.</param>\n    public static void MoveMirrorDown(int mirrorIndex)\n    {\n        if (mirrorIndex > updateMirrors.Count - 2 || mirrorIndex < 0)\n            return;\n\n        (updateMirrors[mirrorIndex], updateMirrors[mirrorIndex + 1]) = (updateMirrors[mirrorIndex + 1], updateMirrors[mirrorIndex]);\n    }\n\n    /// <summary>\n    /// Moves update mirror up in list of update mirrors.\n    /// </summary>\n    /// <param name=\"mirrorIndex\">Index of mirror to move in the list.</param>\n    public static void MoveMirrorUp(int mirrorIndex)\n    {\n        if (updateMirrors.Count <= mirrorIndex || mirrorIndex < 1)\n            return;\n\n        (updateMirrors[mirrorIndex], updateMirrors[mirrorIndex - 1]) = (updateMirrors[mirrorIndex - 1], updateMirrors[mirrorIndex]);\n    }\n\n    /// <summary>\n    /// Returns whether or not there is a currently active custom component in progress.\n    /// </summary>\n    /// <returns>True if custom component download is in progress, otherwise false.</returns>\n    public static bool IsComponentDownloadInProgress()\n    {\n        if (customComponents == null)\n            return false;\n\n        return customComponents.Any(c => c.IsBeingDownloaded);\n    }\n\n    /// <summary>\n    /// Gets custom component index based on name.\n    /// </summary>\n    /// <param name=\"componentName\">Name of custom component.</param>\n    /// <returns>Component index if found, otherwise -1.</returns>\n    public static int GetComponentIndex(string componentName)\n    {\n        if (customComponents == null)\n            return -1;\n\n        return customComponents.FindIndex(c => c.ININame == componentName);\n    }\n\n    /// <summary>\n    /// Get archive info for a file from version file.\n    /// </summary>\n    /// <param name=\"versionFile\">Version file.</param>\n    /// <param name=\"filename\">Filename.</param>\n    /// <param name=\"archiveID\">Set to archive ID.</param>\n    /// <param name=\"archiveSize\">Set to archive file size.</param>\n    internal static void GetArchiveInfo(IniFile versionFile, string filename, out string archiveID, out int archiveSize)\n    {\n        string[] values = versionFile.GetStringValue(\"ArchivedFiles\", filename, string.Empty).Split(',');\n        bool archiveAvailable = values is { Length: >= 2 };\n        archiveID = archiveAvailable ? values[0] : string.Empty;\n        archiveSize = archiveAvailable ? Conversions.IntFromString(values[1], 0) : 0;\n    }\n\n    /// <summary>\n    /// Creates file info instance from given information.\n    /// </summary>\n    /// <param name=\"filename\">Filename.</param>\n    /// <param name=\"identifier\">File identifier.</param>\n    /// <param name=\"size\">File size.</param>\n    /// <param name=\"archiveIdentifier\">Archive file identifier.</param>\n    /// <param name=\"archiveSize\">Archive file size.</param>\n    internal static UpdaterFileInfo CreateFileInfo(string filename, string identifier, int size, string archiveIdentifier = null, int archiveSize = 0)\n    {\n        return new(SafePath.CombineFilePath(filename), size)\n        {\n            Identifier = identifier,\n            ArchiveIdentifier = archiveIdentifier,\n            ArchiveSize = archiveSize\n        };\n    }\n\n    internal static void UpdateUserAgent(HttpClient httpClient)\n    {\n        httpClient.DefaultRequestHeaders.UserAgent.Clear();\n\n        if (GameVersion != \"N/A\")\n            httpClient.DefaultRequestHeaders.UserAgent.Add(new(LocalGame.Replace(' ', '-'), GameVersion.Replace(' ', '-')));\n\n        if (UpdaterVersion != \"N/A\")\n            httpClient.DefaultRequestHeaders.UserAgent.Add(new(nameof(Updater), UpdaterVersion.Replace(' ', '-')));\n\n        httpClient.DefaultRequestHeaders.UserAgent.Add(new(\"Client\", GitVersionInformation.AssemblySemVer));\n    }\n\n    /// <summary>\n    /// Deletes file and waits until it has been deleted.\n    /// </summary>\n    /// <param name=\"filepath\">File to delete.</param>\n    /// <param name=\"timeout\">Maximum time to wait in milliseconds.</param>\n    internal static void DeleteFileAndWait(string filepath, int timeout = 10000)\n    {\n        FileInfo fileInfo = SafePath.GetFile(filepath);\n        using var fw = new FileSystemWatcher(fileInfo.DirectoryName, fileInfo.Name);\n        using var mre = new ManualResetEventSlim();\n\n        fw.EnableRaisingEvents = true;\n        fw.Deleted += (_, _) =>\n        {\n            mre.Set();\n        };\n        if (fileInfo.Exists)\n            fileInfo.IsReadOnly = false;\n        fileInfo.Delete();\n        mre.Wait(timeout);\n    }\n\n    /// <summary>\n    /// Creates all directories required for file path.\n    /// </summary>\n    /// <param name=\"filePath\">File path.</param>\n    internal static void CreatePath(string filePath)\n    {\n        FileInfo fileInfo = SafePath.GetFile(filePath);\n\n        if (!fileInfo.Directory.Exists)\n            fileInfo.Directory.Create();\n    }\n\n    internal static string GetUniqueIdForFile(string filePath)\n    {\n        using var md = MD5.Create();\n        md.Initialize();\n        using FileStream fs = SafePath.GetFile(GamePath, filePath).OpenRead();\n        md.ComputeHash(fs);\n        var builder = new StringBuilder();\n\n        foreach (byte num2 in md.Hash)\n            builder.Append(num2);\n\n        md.Clear();\n        return builder.ToString();\n    }\n\n    /// <summary>\n    /// Parse updater configuration file.\n    /// </summary>\n    private static void ReadUpdaterConfig()\n    {\n        var mirrors = new List<UpdateMirror>();\n        var customComponents = new List<CustomComponent>();\n\n        FileInfo configFile = SafePath.GetFile(ResourcePath, \"UpdaterConfig.ini\");\n\n        if (!configFile.Exists)\n        {\n            Logger.Log(\"Updater config file not found - attempting to read legacy updateconfig.ini.\");\n            ReadLegacyUpdaterConfig(mirrors);\n        }\n        else\n        {\n            var updaterConfig = new IniFile(configFile.FullName);\n            string maskString = updaterConfig.GetStringValue(\"Settings\", \"IgnoreMasks\", string.Join(\",\", ignoreMasks));\n            ignoreMasks = maskString.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);\n\n            List<string> keys = updaterConfig.GetSectionKeys(\"DownloadMirrors\");\n\n            if (keys != null)\n            {\n                foreach (string key in keys)\n                {\n                    if (string.IsNullOrEmpty(key))\n                        continue;\n\n                    string stringValue = updaterConfig.GetStringValue(\"DownloadMirrors\", key, string.Empty);\n\n                    if (string.IsNullOrEmpty(stringValue))\n                        stringValue = key;\n\n                    string[] values = stringValue.Split(',');\n\n                    if (values.Length < 2)\n                        continue;\n                    string url = values[0].Trim().TrimEnd('/') + \"/\";\n\n                    if (mirrors.FindIndex(i => i.URL == url) < 0)\n                        mirrors.Add(new(url, values[1].Trim(), values.Length > 2 ? values[2].Trim() : string.Empty));\n                }\n            }\n\n            keys = updaterConfig.GetSectionKeys(\"CustomComponents\");\n\n            if (keys != null)\n            {\n                foreach (string key in keys)\n                {\n                    if (string.IsNullOrEmpty(key))\n                        continue;\n\n                    string stringValue = updaterConfig.GetStringValue(\"CustomComponents\", key, string.Empty);\n\n                    if (string.IsNullOrEmpty(stringValue))\n                        stringValue = key;\n\n                    string[] values = stringValue.Split(',');\n\n                    if (values.Length < 4)\n                        continue;\n\n                    string ID = values[1].Trim();\n\n                    if (customComponents.FindIndex(i => i.ININame == ID) < 0)\n                    {\n                        string Name = values[0].Trim();\n                        string DownloadPath = values[2].Trim();\n                        string LocalPath = values[3].Trim();\n                        bool noArchiveExtensionForDownloadPath = false;\n\n                        if (values.Length > 4)\n                            noArchiveExtensionForDownloadPath = Conversions.BooleanFromString(values[4], false);\n\n                        bool DownloadPathIsAbsolute = Uri.IsWellFormedUriString(DownloadPath, UriKind.Absolute);\n                        customComponents.Add(new(Name, ID, DownloadPath, LocalPath, DownloadPathIsAbsolute, noArchiveExtensionForDownloadPath));\n                    }\n                }\n            }\n        }\n\n        updateMirrors = mirrors;\n        Updater.customComponents = customComponents;\n\n        if (updateMirrors.Count < 1)\n            Logger.Log(\"Warning: No download mirrors found in updater config file or the built-in game info.\");\n    }\n\n    /// <summary>\n    /// Parse legacy format updater configuration file.\n    /// </summary>\n    /// <param name=\"updateMirrors\">List of update mirrors to add update mirrors to.</param>\n    private static void ReadLegacyUpdaterConfig(List<UpdateMirror> updateMirrors)\n    {\n        FileInfo updateConfigFile = SafePath.GetFile(GamePath, \"updateconfig.ini\");\n\n        if (!updateConfigFile.Exists)\n            return;\n\n        string[] lines;\n\n        try\n        {\n            lines = File.ReadAllLines(updateConfigFile.FullName);\n        }\n        catch (Exception e)\n        {\n            Logger.Log(\"Error: Could not read legacy format updateconfig.ini. Message:\" + e.Message);\n            return;\n        }\n\n        foreach (string line in lines)\n        {\n            if (string.IsNullOrWhiteSpace(line) || line.Trim().StartsWith(';'))\n                continue;\n\n            string[] array = line.Split(new char[] { ',' });\n\n            if (array.Length < 3)\n                continue;\n\n            string url = array[0].Trim().TrimEnd('/') + \"/\";\n            string name = array[1].Trim();\n            string location = array[2].Trim();\n            updateMirrors.Add(new(url, name, location));\n        }\n    }\n\n    /// <summary>\n    /// Performs a version file check on update server.\n    /// </summary>\n    private static async Task DoVersionCheckAsync()\n    {\n        Logger.Log(\"Updater: Doing version file check.\");\n\n        ServerFileInfos.Clear();\n        FileInfosToDownload.Clear();\n        UpdateSizeInKb = 0;\n\n        try\n        {\n            VersionState = VersionState.UPDATECHECKINPROGRESS;\n\n            if (updateMirrors.Count == 0)\n            {\n                Logger.Log(\"Updater: There are no update mirrors!\");\n            }\n            else\n            {\n                Logger.Log(\"Updater: Checking version on the server.\");\n\n                UpdateUserAgent(SharedHttpClient);\n\n                FileInfo versionFile = SafePath.GetFile(GamePath, FormattableString.Invariant($\"{VERSION_FILE}_u\"));\n\n                while (currentUpdateMirrorIndex < updateMirrors.Count)\n                {\n                    try\n                    {\n                        Logger.Log(\"Updater: Trying to connect to update mirror \" + updateMirrors[currentUpdateMirrorIndex].URL);\n\n                        FileStream fileStream = new FileStream(versionFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n                        using (fileStream)\n                        {\n                            Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + VERSION_FILE).ConfigureAwait(false);\n\n                            using (stream)\n                            {\n                                await stream.CopyToAsync(fileStream).ConfigureAwait(false);\n                            }\n                        }\n\n                        break;\n                    }\n                    catch (Exception e)\n                    {\n                        Logger.Log(\"Updater: Error connecting to update mirror. Error message: \" + e.ToString());\n                        Logger.Log(\"Updater: Seeking other mirrors...\");\n                        currentUpdateMirrorIndex++;\n\n                        if (currentUpdateMirrorIndex >= updateMirrors.Count)\n                        {\n                            currentUpdateMirrorIndex = 0;\n                            throw new(\"Unable to connect to update servers.\");\n                        }\n                    }\n                }\n\n                Logger.Log(\"Updater: Downloaded version information.\");\n                var version = new IniFile(versionFile.FullName);\n                string versionString = version.GetStringValue(\"DTA\", \"Version\", string.Empty);\n                string updaterVersionString = version.GetStringValue(\"DTA\", \"UpdaterVersion\", \"N/A\");\n                string manualDownloadURLString = version.GetStringValue(\"DTA\", \"ManualDownloadURL\", string.Empty);\n\n                if (version.SectionExists(\"FileVersions\"))\n                {\n                    foreach (string key in version.GetSectionKeys(\"FileVersions\"))\n                    {\n                        string[] tmp = version.GetStringValue(\"FileVersions\", key, string.Empty).Split(',');\n\n                        if (tmp.Length < 2)\n                        {\n                            Logger.Log(\"Updater: Warning: Malformed file info in downloaded version information: \" + key);\n                            continue;\n                        }\n\n                        GetArchiveInfo(version, key, out string archiveID, out int archiveSize);\n                        UpdaterFileInfo item = CreateFileInfo(key, tmp[0], Conversions.IntFromString(tmp[1], 0), archiveID, archiveSize);\n                        ServerFileInfos.Add(item);\n                    }\n                }\n\n                if (version.SectionExists(\"AddOns\"))\n                {\n                    foreach (string key in version.GetSectionKeys(\"AddOns\"))\n                    {\n                        string[] tmp = version.GetStringValue(\"AddOns\", key, string.Empty).Split(',');\n\n                        if (tmp.Length < 2)\n                        {\n                            Logger.Log(\"Updater: Warning: Malformed addon info in downloaded version information: \" + key);\n                            continue;\n                        }\n\n                        UpdaterFileInfo item = CreateFileInfo(key, tmp[0], Conversions.IntFromString(tmp[1], 0), string.Empty, 0);\n                        int index = GetComponentIndex(key);\n\n                        if (index == -1)\n                        {\n                            Logger.Log(\"Updater: Warning: Invalid custom component ID \" + key);\n                        }\n                        else\n                        {\n                            CustomComponent component = customComponents[index];\n                            component.Initialized = false;\n                            Logger.Log(\"Updater: Setting custom component info for \" + key);\n                            GetArchiveInfo(version, component.LocalPath, out string archiveID, out int archiveSize);\n                            item.ArchiveIdentifier = archiveID;\n                            item.ArchiveSize = archiveSize;\n                            component.RemoteSize = item.Size * 1024;\n                            component.RemoteArchiveSize = item.Archived ? item.ArchiveSize * 1024 : 0;\n                            component.RemoteIdentifier = item.Identifier;\n                            component.Archived = item.Archived;\n\n                            if (SafePath.GetFile(GamePath, component.LocalPath).Exists)\n                                component.LocalIdentifier = GetUniqueIdForFile(component.LocalPath);\n\n                            component.Initialized = true;\n                        }\n                    }\n                }\n\n                if (string.IsNullOrEmpty(versionString))\n                    throw new(\"Update server integrity error while checking for updates.\");\n\n                Logger.Log(\"Updater: Server game version is \" + versionString + \", local version is \" + GameVersion);\n                ServerGameVersion = versionString;\n\n                if (versionString == GameVersion)\n                {\n                    VersionState = VersionState.UPTODATE;\n                    versionFile.Delete();\n                    DoFileIdentifiersUpdatedEvent();\n\n                    if (AreCustomComponentsOutdated())\n                        DoCustomComponentsOutdatedEvent();\n                }\n                else\n                {\n                    if (updaterVersionString != \"N/A\" && UpdaterVersion != updaterVersionString)\n                    {\n                        Logger.Log(\"Updater: Server update system version is set to \" + updaterVersionString + \" and is different to local update system version \" + UpdaterVersion + \". Manual update required.\");\n                        VersionState = VersionState.OUTDATED;\n                        ManualUpdateRequired = true;\n                        ManualDownloadURL = manualDownloadURLString;\n                        versionFile.Delete();\n                        DoFileIdentifiersUpdatedEvent();\n                    }\n                    else\n                    {\n                        VersionCheckHandle();\n                    }\n                }\n            }\n        }\n        catch (Exception exception)\n        {\n            VersionState = VersionState.UNKNOWN;\n            Logger.Log(\"Updater: An error occured while performing version check: \" + exception.Message);\n            DoFileIdentifiersUpdatedEvent();\n        }\n    }\n\n    /// <summary>\n    /// Checks if custom components are outdated.\n    /// </summary>\n    /// <returns>True if custom components are outdated, otherwise false.</returns>\n    private static bool AreCustomComponentsOutdated()\n    {\n        Logger.Log(\"Updater: Checking if custom components are outdated.\");\n        foreach (CustomComponent component in customComponents)\n        {\n            if (SafePath.GetFile(GamePath, component.LocalPath).Exists && component.RemoteIdentifier != component.LocalIdentifier)\n                return true;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// Executes after-update script file.\n    /// </summary>\n    private static async ValueTask ExecuteAfterUpdateScriptAsync()\n    {\n        Logger.Log(\"Updater: Downloading updateexec.\");\n        try\n        {\n            string downloadFile = SafePath.CombineFilePath(GamePath, \"updateexec\");\n\n            FileStream fileStream = new FileStream(downloadFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n            using (fileStream)\n            {\n                Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + \"updateexec\").ConfigureAwait(false);\n\n                using (stream)\n                {\n                    await stream.CopyToAsync(fileStream).ConfigureAwait(false);\n                }\n            }\n        }\n        catch (Exception exception)\n        {\n            Logger.Log(\"Updater: Warning: Downloading updateexec failed: \" + exception.Message);\n            return;\n        }\n\n        ExecuteScript(\"updateexec\");\n    }\n\n    /// <summary>\n    /// Executes pre-update script file.\n    /// </summary>\n    /// <returns>True if succesful, otherwise false.</returns>\n    private static async ValueTask<bool> ExecutePreUpdateScriptAsync()\n    {\n        Logger.Log(\"Updater: Downloading preupdateexec.\");\n        try\n        {\n            string downloadFile = SafePath.CombineFilePath(GamePath, \"preupdateexec\");\n\n            FileStream fileStream = new FileStream(downloadFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n\n            using (fileStream)\n            {\n                Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + \"preupdateexec\").ConfigureAwait(false);\n\n                using (stream)\n                {\n                    await stream.CopyToAsync(fileStream).ConfigureAwait(false);\n                }\n            }\n        }\n        catch (Exception exception)\n        {\n            Logger.Log(\"Updater: Warning: Downloading preupdateexec failed: \" + exception.Message);\n            return false;\n        }\n\n        ExecuteScript(\"preupdateexec\");\n        return true;\n    }\n\n    /// <summary>\n    /// Executes a script file.\n    /// </summary>\n    /// <param name=\"fileName\">Filename of the script file.</param>\n    private static void ExecuteScript(string fileName)\n    {\n        Logger.Log(\"Updater: Executing \" + fileName + \".\");\n        FileInfo scriptFileInfo = SafePath.GetFile(GamePath, fileName);\n        var script = new IniFile(scriptFileInfo.FullName);\n\n        // Delete files.\n        foreach (string key in GetKeys(script, \"Delete\"))\n        {\n            Logger.Log(\"Updater: \" + fileName + \": Deleting file \" + key);\n\n            try\n            {\n                FileInfo fileInfo = SafePath.GetFile(GamePath, key);\n\n                if (fileInfo.Exists)\n                {\n                    fileInfo.IsReadOnly = false;\n                    fileInfo.Delete();\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Deleting file \" + key + \"failed: \" + ex.Message);\n            }\n        }\n\n        // Rename files.\n        foreach (string key in GetKeys(script, \"Rename\"))\n        {\n            string newFilename = SafePath.CombineFilePath(script.GetStringValue(\"Rename\", key, string.Empty));\n            if (string.IsNullOrWhiteSpace(newFilename))\n                continue;\n            try\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Renaming file '\" + key + \"' to '\" + newFilename + \"'\");\n\n                FileInfo srcFile = SafePath.GetFile(GamePath, key);\n\n                if (srcFile.Exists)\n                {\n                    bool isSrcReadOnly = srcFile.IsReadOnly;\n                    srcFile.IsReadOnly = false;\n\n                    {\n                        FileInfo destFile = SafePath.GetFile(GamePath, newFilename);\n                        if (destFile.Exists)\n                        {\n                            destFile.IsReadOnly = false;\n                            destFile.Delete();\n                        }\n                    }\n\n                    srcFile.MoveTo(SafePath.CombineFilePath(GamePath, newFilename));\n\n                    if (isSrcReadOnly)\n                    {\n                        FileInfo destFile = SafePath.GetFile(GamePath, newFilename);\n                        destFile.IsReadOnly = true;\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Renaming file '\" + key + \"' to '\" + newFilename + \"' failed: \" + ex.Message);\n            }\n        }\n\n        // Rename folders.\n        foreach (string key in GetKeys(script, \"RenameFolder\"))\n        {\n            string newDirectoryName = script.GetStringValue(\"RenameFolder\", key, string.Empty);\n            if (string.IsNullOrWhiteSpace(newDirectoryName))\n                continue;\n            try\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Renaming directory '\" + key + \"' to '\" + newDirectoryName + \"'\");\n\n                DirectoryInfo srcDirectory = SafePath.GetDirectory(GamePath, key);\n\n                if (srcDirectory.Exists)\n                    srcDirectory.MoveTo(SafePath.CombineDirectoryPath(GamePath, newDirectoryName));\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Renaming directory '\" + key + \"' to '\" + newDirectoryName + \"' failed: \" + ex.Message);\n            }\n        }\n\n        // Rename & merge files / folders.\n        foreach (string key in GetKeys(script, \"RenameAndMerge\"))\n        {\n            string directoryName = key;\n            string directoryNameToMergeInto = script.GetStringValue(\"RenameAndMerge\", key, string.Empty);\n            if (string.IsNullOrWhiteSpace(directoryNameToMergeInto))\n                continue;\n            try\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Merging directory '\" + directoryName + \"' with '\" + directoryNameToMergeInto + \"'\");\n                DirectoryInfo directoryToMergeInto = SafePath.GetDirectory(GamePath, directoryNameToMergeInto);\n                DirectoryInfo gameDirectory = SafePath.GetDirectory(GamePath, directoryName);\n\n                if (!gameDirectory.Exists)\n                    continue;\n\n                if (!directoryToMergeInto.Exists)\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Destination directory '\" + directoryNameToMergeInto + \"' does not exist, renaming.\");\n                    gameDirectory.MoveTo(directoryToMergeInto.FullName);\n                }\n                else\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Destination directory '\" + directoryNameToMergeInto + \"' exists, performing selective merging.\");\n                    FileInfo[] files = gameDirectory.GetFiles();\n                    foreach (FileInfo file in files)\n                    {\n                        bool isSrcReadOnly = file.IsReadOnly;\n                        file.IsReadOnly = false;\n\n                        FileInfo fileToMergeInto = SafePath.GetFile(directoryToMergeInto.FullName, file.Name);\n                        if (fileToMergeInto.Exists)\n                        {\n                            Logger.Log(\"Updater: \" + fileName + \": Destination file '\" + directoryNameToMergeInto + \"/\" + file.Name +\n                                \"' exists, removing original source file \" + directoryName + \"/\" + file.Name);\n\n                            // Note: Previously, the incorrect file was deleted as of commit fc939a06ff978b51daa6563eaa15a28cf48319ec.\n\n                            // Remove the original source file\n                            file.Delete();\n                        }\n                        else\n                        {\n                            Logger.Log(\"Updater: \" + fileName + \": Destination file '\" + directoryNameToMergeInto + \"/\" + file.Name +\n                                \"' does not exist, moving original source file \" + directoryName + \"/\" + file.Name);\n                            file.MoveTo(fileToMergeInto.FullName);\n\n                            // Resume the read-only property\n                            fileToMergeInto.Refresh();\n                            fileToMergeInto.IsReadOnly = isSrcReadOnly;\n                        }\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Merging directory '\" + directoryName + \"' with '\" + directoryNameToMergeInto + \"' failed: \" + ex.Message);\n            }\n        }\n\n        // Delete folders.\n        foreach (string sectionName in new string[] { \"DeleteFolder\", \"ForceDeleteFolder\" })\n        {\n            foreach (string key in GetKeys(script, sectionName))\n            {\n                try\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Deleting directory '\" + key + \"'\");\n\n                    DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key);\n                    if (directoryInfo.Exists)\n                    {\n                        // Unset read-only attribute from all files in the directory.\n                        foreach (FileInfo file in directoryInfo.GetFiles(\"*\", SearchOption.AllDirectories))\n                        {\n                            file.IsReadOnly = false;\n                        }\n\n                        directoryInfo.Delete(true);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Deleting directory '\" + key + \"' failed: \" + ex.Message);\n                }\n            }\n        }\n\n        // Delete folders, if empty.\n        foreach (string key in GetKeys(script, \"DeleteFolderIfEmpty\"))\n        {\n            try\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Deleting directory '\" + key + \"' if it's empty.\");\n\n                DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key);\n\n                if (directoryInfo.Exists)\n                {\n                    if (!directoryInfo.EnumerateFiles().Any())\n                    {\n                        directoryInfo.Delete();\n                    }\n                    else\n                    {\n                        Logger.Log(\"Updater: \" + fileName + \": Directory '\" + key + \"' is not empty!\");\n                    }\n                }\n                else\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Specified directory does not exist.\");\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Deleting directory '\" + key + \"' if it's empty failed: \" + ex.Message);\n            }\n        }\n\n        // Create folders.\n        foreach (string key in GetKeys(script, \"CreateFolder\"))\n        {\n            try\n            {\n                DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key);\n                if (!directoryInfo.Exists)\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Creating directory '\" + key + \"'\");\n                    directoryInfo.Create();\n                }\n                else\n                {\n                    Logger.Log(\"Updater: \" + fileName + \": Directory '\" + key + \"' already exists.\");\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Updater: \" + fileName + \": Creating directory '\" + key + \"' failed: \" + ex.Message);\n            }\n        }\n\n        scriptFileInfo.Delete();\n    }\n\n    /// <summary>\n    /// Handle version check.\n    /// </summary>\n    private static void VersionCheckHandle()\n    {\n        Logger.Log(\"Updater: Gathering list of files to be downloaded. Server file info count: \" + ServerFileInfos.Count);\n\n        FileInfosToDownload.Clear();\n\n        for (int i = 0; i < ServerFileInfos.Count; i++)\n        {\n            string identifier = ServerFileInfos[i].Identifier;\n            bool flag = false;\n            FileInfo serverFileInfo = SafePath.GetFile(GamePath, ServerFileInfos[i].Filename);\n\n            for (int k = 0; k < LocalFileInfos.Count; k++)\n            {\n                UpdaterFileInfo info = LocalFileInfos[k];\n\n                if (ServerFileInfos[i].Filename == info.Filename)\n                {\n                    flag = true;\n\n                    if (!serverFileInfo.Exists)\n                    {\n                        Logger.Log(\"Updater: File \" + ServerFileInfos[i].Filename + \" not found. Adding it to the download queue.\");\n                        FileInfosToDownload.Add(ServerFileInfos[i]);\n                    }\n                    else if (info.Identifier != identifier)\n                    {\n                        Logger.Log(\"Updater: Local file \" + info.Filename + \" is different, adding it to the download queue.\");\n                        FileInfosToDownload.Add(ServerFileInfos[i]);\n                    }\n                }\n            }\n\n            if (!flag)\n            {\n                Logger.Log(\"Updater: File \" + ServerFileInfos[i].Filename + \" doesn't exist on local version information - checking if it exists in the directory.\");\n\n                if (serverFileInfo.Exists)\n                {\n                    if (TryGetUniqueId(ServerFileInfos[i].Filename) != identifier)\n                    {\n                        Logger.Log(\"Updater: File \" + ServerFileInfos[i].Filename + \" is out of date. Adding it to the download queue.\");\n                        FileInfosToDownload.Add(ServerFileInfos[i]);\n                    }\n                    else\n                    {\n                        Logger.Log(\"Updater: File \" + ServerFileInfos[i].Filename + \" exists in the directory and is up to date.\");\n                    }\n                }\n                else\n                {\n                    Logger.Log(\"Updater: File \" + ServerFileInfos[i].Filename + \" not found. Adding it to the download queue.\");\n                    FileInfosToDownload.Add(ServerFileInfos[i]);\n                }\n            }\n        }\n\n        UpdateSizeInKb = 0;\n\n        for (int j = 0; j < FileInfosToDownload.Count; j++)\n        {\n            UpdateSizeInKb += FileInfosToDownload[j].Archived ? FileInfosToDownload[j].ArchiveSize : FileInfosToDownload[j].Size;\n        }\n\n        VersionState = VersionState.OUTDATED;\n        ManualUpdateRequired = false;\n        DoFileIdentifiersUpdatedEvent();\n    }\n\n    /// <summary>\n    /// Verifies local file version info.\n    /// </summary>\n    private static void VerifyLocalFileVersions()\n    {\n        Logger.Log(\"Verifying local file versions. Count: \" + LocalFileInfos.Count);\n        for (int i = 0; i < LocalFileInfos.Count; i++)\n        {\n            UpdaterFileInfo info = LocalFileInfos[i];\n            if (!ContainsAnyMask(info.Filename))\n            {\n                if (SafePath.GetFile(GamePath, info.Filename).Exists)\n                {\n                    string uniqueIdForFile = GetUniqueIdForFile(info.Filename);\n                    if (uniqueIdForFile != info.Identifier)\n                    {\n                        Logger.Log(\"Invalid unique identifier for \" + info.Filename + \"!\");\n                        info.Identifier = uniqueIdForFile;\n                    }\n                }\n                else\n                {\n                    Logger.Log(\"File \" + info.Filename + \" does not exist!\");\n                    LocalFileInfos.RemoveAt(i);\n                    i--;\n                }\n\n                if (LocalFileInfos.Count > 0)\n                    LocalFileCheckProgressChanged?.Invoke(i + 1, LocalFileInfos.Count);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Downloads files required for update and starts second-stage updater.\n    /// </summary>\n    private static async Task PerformUpdateAsync()\n    {\n        Logger.Log(\"Updater: Starting update.\");\n        VersionState = VersionState.UPDATEINPROGRESS;\n\n        try\n        {\n            UpdateUserAgent(SharedHttpClient);\n\n            SharedProgressMessageHandler.HttpReceiveProgress += ProgressMessageHandlerOnHttpReceiveProgress;\n\n            if (!await ExecutePreUpdateScriptAsync().ConfigureAwait(false))\n                throw new(\"Executing preupdateexec failed.\");\n\n            VerifyLocalFileVersions();\n            VersionCheckHandle();\n\n            if (string.IsNullOrEmpty(ServerGameVersion) || ServerGameVersion == \"N/A\" || VersionState != VersionState.OUTDATED)\n                throw new(\"Update server integrity error.\");\n\n            VersionState = VersionState.UPDATEINPROGRESS;\n\n            totalDownloadedKbs = 0;\n\n            if (terminateUpdate)\n            {\n                Logger.Log(\"Updater: Terminating update because of user request.\");\n                VersionState = VersionState.OUTDATED;\n                ManualUpdateRequired = false;\n                terminateUpdate = false;\n            }\n            else\n            {\n                foreach (UpdaterFileInfo info in FileInfosToDownload)\n                {\n                    int num = 0;\n\n                    if (terminateUpdate)\n                    {\n                        Logger.Log(\"Updater: Terminating update because of user request.\");\n                        VersionState = VersionState.OUTDATED;\n                        ManualUpdateRequired = false;\n                        terminateUpdate = false;\n                        return;\n                    }\n\n                    while (true)\n                    {\n                        currentFilename = info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename;\n                        currentFileSize = info.Archived ? info.ArchiveSize : info.Size;\n                        string errorMessage = await DownloadFileAsync(info).ConfigureAwait(false);\n\n                        if (terminateUpdate)\n                        {\n                            Logger.Log(\"Updater: Terminating update because of user request.\");\n                            VersionState = VersionState.OUTDATED;\n                            ManualUpdateRequired = false;\n                            terminateUpdate = false;\n                            return;\n                        }\n\n                        if (errorMessage is null)\n                        {\n                            totalDownloadedKbs += info.Archived ? info.ArchiveSize : info.Size;\n                            break;\n                        }\n\n                        num++;\n\n                        if (num == 2)\n                        {\n                            Logger.Log(\"Updater: Too many retries for downloading file \" +\n                                (info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename) + \". Update halted.\");\n\n                            string extraMsg = Environment.NewLine + Environment.NewLine + \"Download error message: \" + errorMessage;\n\n                            throw new(\"Too many retries for downloading file \" +\n                                      (info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename) + extraMsg);\n                        }\n                    }\n                }\n\n                if (terminateUpdate)\n                {\n                    Logger.Log(\"Updater: Terminating update because of user request.\");\n                    VersionState = VersionState.OUTDATED;\n                    ManualUpdateRequired = false;\n                    terminateUpdate = false;\n                }\n                else\n                {\n                    Logger.Log(\"Updater: Downloading files finished - copying from temporary updater directory.\");\n                    await ExecuteAfterUpdateScriptAsync().ConfigureAwait(false);\n                    Logger.Log(\"Updater: Cleaning up.\");\n\n                    // this folder contains incoming files that needs to be updated by second stage updater\n                    DirectoryInfo incomingDirectoryInfo = SafePath.GetDirectory(GamePath, \"Updater\");\n                    FileInfo versionFile = SafePath.GetFile(GamePath, VERSION_FILE);\n                    FileInfo versionFileTemp = SafePath.GetFile(GamePath, FormattableString.Invariant($\"{VERSION_FILE}_u\"));\n\n                    if (incomingDirectoryInfo.Exists)\n                    {\n                        versionFileTemp.MoveTo(SafePath.CombineFilePath(incomingDirectoryInfo.FullName, VERSION_FILE));\n\n                        // make sure the existing version file do not exist, to make the legacy \"clientupdt.exe\" second stage updater happy\n                        SafePath.DeleteFileIfExists(versionFile.FullName);\n                    }\n                    else\n                    {\n                        // since second stage updater will not be launched, just override the existing version file\n                        SafePath.DeleteFileIfExists(versionFile.FullName);\n                        versionFileTemp.MoveTo(versionFile.FullName);\n                    }\n\n                    FileInfo themeFileInfo = SafePath.GetFile(GamePath, \"Theme_c.ini\");\n\n                    if (themeFileInfo.Exists)\n                    {\n                        Logger.Log(\"Updater: Theme_c.ini exists -- copying it.\");\n                        themeFileInfo.CopyTo(SafePath.CombineFilePath(GamePath, \"INI\", \"Theme.ini\"), true);\n                        Logger.Log(\"Updater: Theme.ini copied successfully.\");\n                    }\n\n                    incomingDirectoryInfo.Refresh();\n\n                    if (incomingDirectoryInfo.Exists)\n                    {\n                        // update legacy second stage updater\n                        DirectoryInfo currentLegacySecondStageUpdaterDirectory = SafePath.GetDirectory(GamePath);\n                        FileInfo currentLegacySecondStageUpdaterExecutable = SafePath.GetFile(currentLegacySecondStageUpdaterDirectory.FullName, LEGACY_SECOND_STAGE_UPDATER);\n                        DirectoryInfo incomingLegacySecondStageUpdaterDirectory = SafePath.GetDirectory(incomingDirectoryInfo.FullName);\n                        FileInfo incomingLegacySecondStageUpdaterExecutable = SafePath.GetFile(incomingLegacySecondStageUpdaterDirectory.FullName, LEGACY_SECOND_STAGE_UPDATER);\n                        if (incomingLegacySecondStageUpdaterExecutable.Exists)\n                        {\n                            SafePath.DeleteFileIfExists(currentLegacySecondStageUpdaterExecutable.FullName);\n                            incomingLegacySecondStageUpdaterExecutable.MoveTo(currentLegacySecondStageUpdaterExecutable.FullName);\n                            currentLegacySecondStageUpdaterExecutable.Refresh();\n                        }\n\n                        #region update-second-stage-updater\n\n                        // the second stage updater is placed at \"Resources\\Binaries\\Updater\" directory.\n                        DirectoryInfo currentSecondStageUpdaterDirectory = SafePath.GetDirectory(ResourcePath, BINARIES_FOLDER, \"Updater\");\n                        if (!currentSecondStageUpdaterDirectory.Exists)\n                            currentSecondStageUpdaterDirectory.Create();\n\n                        FileInfo secondStageUpdaterExecutable = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, SECOND_STAGE_UPDATER);\n\n                        // update the new second stage updater before other files\n                        DirectoryInfo incomingSecondStageUpdaterDirectory = SafePath.GetDirectory(incomingDirectoryInfo.FullName, \"Resources\", BINARIES_FOLDER, \"Updater\");\n                        if (incomingSecondStageUpdaterDirectory.Exists)\n                        {\n                            Logger.Log(\"Updater: Checking & moving second-stage updater files.\");\n\n                            // copy SecondStageUpdater\n                            IEnumerable<FileInfo> updaterFiles = incomingSecondStageUpdaterDirectory.EnumerateFiles(Path.GetFileNameWithoutExtension(SECOND_STAGE_UPDATER) + \".*\");\n\n                            foreach (FileInfo updaterFile in updaterFiles)\n                            {\n                                FileInfo updaterFileResource = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, updaterFile.Name);\n\n                                Logger.Log(\"Updater: Moving second-stage updater file \" + updaterFile.Name + \".\");\n\n                                SafePath.DeleteFileIfExists(updaterFileResource.FullName);\n                                updaterFile.MoveTo(updaterFileResource.FullName);\n                            }\n\n                            // copy SecondStageUpdater dependencies\n                            // warning: for unknown reasons, `System.Runtime.CompilerServices.Unsafe.dll` file is not listed here.\n                            // Therefore, Polyfill (requiring this dll file) is excluded from the second-stage updater.\n                            AssemblyName[] assemblies = Assembly.LoadFrom(secondStageUpdaterExecutable.FullName).GetReferencedAssemblies();\n\n                            foreach (AssemblyName assembly in assemblies)\n                            {\n                                FileInfo incomingAssemblyFile = SafePath.GetFile(incomingSecondStageUpdaterDirectory.FullName, FormattableString.Invariant($\"{assembly.Name}.dll\"));\n\n                                if (!incomingAssemblyFile.Exists)\n                                {\n                                    Logger.Log(\"Updater: Missing assembly file required by second-stage updater: \" + incomingAssemblyFile.Name + \".\");\n                                    continue;\n                                }\n\n                                FileInfo currentAssemblyFile = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, incomingAssemblyFile.Name);\n\n                                Logger.Log(\"Updater: Moving second-stage updater file \" + incomingAssemblyFile.Name + \".\");\n\n                                SafePath.DeleteFileIfExists(currentAssemblyFile.FullName);\n                                incomingAssemblyFile.MoveTo(currentAssemblyFile.FullName);\n                            }\n                        }\n                        #endregion\n\n                        Logger.Log(\"Updater: Launching second-stage updater executable \" + secondStageUpdaterExecutable.FullName + \".\");\n\n                        // fallback to the old \"clientupdt.dat\" file if the new second-stage updater does not exist\n                        bool runNativeWindowsExe = true;\n#if !NETFRAMEWORK\n                        runNativeWindowsExe = false;\n#endif\n\n                        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !secondStageUpdaterExecutable.Exists)\n                        {\n                            Logger.Log(\"Updater: Missing second-stage updater executable \" + secondStageUpdaterExecutable.FullName + \".\");\n                            if (currentLegacySecondStageUpdaterExecutable.Exists)\n                            {\n                                Logger.Log(\"Updater: Falling back to legacy second-stage updater executable \" + currentLegacySecondStageUpdaterExecutable.FullName + \".\");\n                                secondStageUpdaterExecutable = currentLegacySecondStageUpdaterExecutable;\n                                runNativeWindowsExe = true;\n                            }\n                        }\n\n                        ProcessStartInfo secondStageUpdaterStartInfo;\n                        if (runNativeWindowsExe)\n                        {\n                            // e.g. C:\\Game\\Resources\\SecondStageUpdater.exe clientogl.exe \"C:\\Game\\\"\n                            secondStageUpdaterStartInfo = new ProcessStartInfo\n                            {\n                                FileName = secondStageUpdaterExecutable.FullName,\n                                Arguments = CallingExecutableFileName + \" \\\"\" + GamePath + \"\\\"\",\n                                UseShellExecute = false,\n                            };\n                        }\n                        else\n                        {\n                            // e.g. dotnet \"C:\\Game\\Resources\\SecondStageUpdater.dll\" clientogl.dll \"C:\\Game\\\"\n                            secondStageUpdaterStartInfo = new ProcessStartInfo\n                            {\n                                FileName = \"dotnet\",\n                                Arguments = \"\\\"\" + secondStageUpdaterExecutable.FullName + \"\\\" \" + CallingExecutableFileName + \" \\\"\" + GamePath + \"\\\"\",\n                                UseShellExecute = true,\n                            };\n                        }\n\n                        Logger.Log(\"Updater: Launching second-stage updater executable.\");\n                        Logger.Log(\"Updater: FileName = \" + secondStageUpdaterStartInfo.FileName);\n                        Logger.Log(\"Updater: Arguments = \" + secondStageUpdaterStartInfo.Arguments);\n                        Logger.Log(\"Updater: UseShellExecute = \" + secondStageUpdaterStartInfo.UseShellExecute);\n                        using var _ = Process.Start(secondStageUpdaterStartInfo);\n\n                        Restart?.Invoke(null, EventArgs.Empty);\n                    }\n                    else\n                    {\n                        Logger.Log(\"Updater: Update completed successfully.\");\n                        totalDownloadedKbs = 0;\n                        UpdateSizeInKb = 0;\n                        CheckLocalFileVersions();\n                        ServerGameVersion = \"N/A\";\n                        VersionState = VersionState.UPTODATE;\n                        DoUpdateCompleted();\n\n                        if (AreCustomComponentsOutdated())\n                            DoCustomComponentsOutdatedEvent();\n                    }\n                }\n            }\n        }\n        catch (Exception exception)\n        {\n            Logger.Log(\"Updater: An error occurred during the update. Message: \" + exception.Message);\n            VersionState = VersionState.UNKNOWN;\n            DoOnUpdateFailed(exception);\n        }\n        finally\n        {\n            SharedProgressMessageHandler.HttpReceiveProgress -= ProgressMessageHandlerOnHttpReceiveProgress;\n        }\n    }\n\n    /// <summary>\n    /// Downloads and handles individual file.\n    /// </summary>\n    /// <param name=\"fileInfo\">File info for the file.</param>\n    /// <returns>Error message if something went wrong, otherwise null.</returns>\n    private static async ValueTask<string> DownloadFileAsync(UpdaterFileInfo fileInfo)\n    {\n        Logger.Log(\"Updater: Initializing download of file \" + fileInfo.Filename);\n\n        UpdateDownloadProgress(0);\n\n        string filename = fileInfo.Filename;\n        const string prefixPath = \"Updater\";\n        FileInfo decompressedFile = SafePath.GetFile(GamePath, prefixPath, filename);\n\n        try\n        {\n            string uriString = string.Empty;\n            int currentUpdateMirrorId = Updater.currentUpdateMirrorIndex;\n            string extraExtension = fileInfo.Archived ? ARCHIVE_FILE_EXTENSION : string.Empty;\n            string fileRelativePath = SafePath.CombineFilePath(prefixPath, FormattableString.Invariant($\"{filename}{extraExtension}\"));\n            uriString = (updateMirrors[currentUpdateMirrorId].URL + filename + extraExtension).Replace('\\\\', '/');\n            FileInfo downloadFile = SafePath.GetFile(GamePath, fileRelativePath);\n            CreatePath(SafePath.CombineFilePath(GamePath, filename));\n            CreatePath(downloadFile.FullName);\n\n            if (downloadFile.Exists &&\n                (fileInfo.Archived ? fileInfo.Identifier : fileInfo.ArchiveIdentifier) == GetUniqueIdForFile(fileRelativePath))\n            {\n                Logger.Log(\"Updater: File \" + filename + \" has already been downloaded, skipping downloading.\");\n            }\n            else\n            {\n                Logger.Log(\"Updater: Downloading file \" + filename + extraExtension);\n\n                FileStream fileStream = new FileStream(downloadFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);\n                using (fileStream)\n                {\n                    Stream stream = await SharedHttpClient.GetStreamAsync(new Uri(uriString)).ConfigureAwait(false);\n                    using (stream)\n                    {\n                        await stream.CopyToAsync(fileStream).ConfigureAwait(false);\n                    }\n                }\n\n                OnFileDownloadCompleted?.Invoke(fileInfo.Archived ? filename + extraExtension : null);\n                Logger.Log(\"Updater: Download of file \" + filename + extraExtension + \" finished - verifying.\");\n\n                if (fileInfo.Archived)\n                {\n                    Logger.Log(\"Updater: File is an archive.\");\n                    string archiveIdentifier = CheckFileIdentifiers(filename, fileRelativePath, fileInfo.ArchiveIdentifier);\n\n                    if (string.IsNullOrEmpty(archiveIdentifier))\n                    {\n                        Logger.Log(\"Updater: Archive \" + filename + extraExtension + \" is intact. Unpacking...\");\n                        await CompressionHelper.DecompressFileAsync(downloadFile.FullName, decompressedFile.FullName).ConfigureAwait(false);\n                        downloadFile.Delete();\n                    }\n                    else\n                    {\n                        string errorMsg = \"Downloaded archive \" + filename + extraExtension + \" has a non-matching identifier: \" + archiveIdentifier + \" against \" + fileInfo.ArchiveIdentifier;\n                        Logger.Log(\"Updater: \" + errorMsg);\n                        DeleteFileAndWait(downloadFile.FullName);\n\n                        return errorMsg;\n                    }\n                }\n#if !NETFRAMEWORK\n\n                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && downloadFile.Extension.Equals(\".sh\", StringComparison.OrdinalIgnoreCase))\n                {\n                    Logger.Log($\"Updater: File {downloadFile.Name} is a script, adding execute permission. Current permission flags: \" + downloadFile.UnixFileMode);\n\n                    downloadFile.Refresh();\n\n                    downloadFile.UnixFileMode |= UnixFileMode.UserExecute;\n\n                    downloadFile.Refresh();\n\n                    Logger.Log($\"Updater: File {downloadFile.Name} execute permission added. Current permission flags: \" + downloadFile.UnixFileMode);\n                }\n#endif\n            }\n\n            string fileIdentifier = CheckFileIdentifiers(filename, SafePath.CombineFilePath(prefixPath, filename), fileInfo.Identifier);\n            if (string.IsNullOrEmpty(fileIdentifier))\n            {\n                Logger.Log(\"Updater: File \" + filename + \" is intact.\");\n\n                return null;\n            }\n\n            string msg = \"Downloaded file \" + filename + \" has a non-matching identifier: \" + fileIdentifier + \" against \" + fileInfo.Identifier;\n            Logger.Log(\"Updater: \" + msg);\n            DeleteFileAndWait(decompressedFile.FullName);\n\n            return msg;\n        }\n        catch (Exception exception)\n        {\n            Logger.Log(\"Updater: An error occurred while downloading file \" + filename + \": \" + exception.Message);\n            DeleteFileAndWait(decompressedFile.FullName);\n\n            return exception.Message;\n        }\n    }\n\n    /// <summary>\n    /// Updates download progress.\n    /// </summary>\n    /// <param name=\"progressPercentage\">Progress percentage.</param>\n    private static void UpdateDownloadProgress(int progressPercentage)\n    {\n        double num = currentFileSize * (progressPercentage / 100.0);\n        double num2 = totalDownloadedKbs + num;\n\n        int totalPercentage = 0;\n\n        if (UpdateSizeInKb is > 0 and < int.MaxValue)\n            totalPercentage = (int)(num2 / UpdateSizeInKb * 100.0);\n\n        DownloadProgressChanged(currentFilename, progressPercentage, totalPercentage);\n    }\n\n    /// <summary>\n    /// Checks if file path contains ignore masks.\n    /// </summary>\n    /// <param name=\"filePath\">File path to check.</param>\n    /// <returns>True if path contains any ignore masks, otherwise false.</returns>\n    private static bool ContainsAnyMask(string filePath)\n    {\n        foreach (string mask in ignoreMasks)\n        {\n            if (filePath.Contains(mask, StringComparison.OrdinalIgnoreCase))\n                return true;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// Gets keys from INI file section.\n    /// </summary>\n    /// <param name=\"iniFile\">INI file.</param>\n    /// <param name=\"sectionName\">Section name.</param>\n    /// <returns>List of keys or empty list if section does not exist or no keys were found.</returns>\n    private static List<string> GetKeys(IniFile iniFile, string sectionName)\n    {\n        List<string> keys = iniFile.GetSectionKeys(sectionName);\n\n        if (keys != null)\n            return keys;\n\n        return new();\n    }\n\n    /// <summary>\n    /// Attempts to get file identifier for a file.\n    /// </summary>\n    /// <param name=\"filePath\">File path of file.</param>\n    /// <returns>File identifier if successful, otherwise empty string.</returns>\n    private static string TryGetUniqueId(string filePath)\n    {\n        try\n        {\n            return GetUniqueIdForFile(filePath);\n        }\n        catch\n        {\n            return string.Empty;\n        }\n    }\n\n    /// <summary>\n    /// Checks file identifiers to see if file is intact.\n    /// </summary>\n    /// <param name=\"fileInfoFilename\">Filename in file info.</param>\n    /// <param name=\"localFilename\">Filename on system.</param>\n    /// <param name=\"fileInfoIdentifier\">Current file identifier.</param>\n    /// <returns>File identifier if check is successful, otherwise null.</returns>\n    private static string CheckFileIdentifiers(string fileInfoFilename, string localFilename, string fileInfoIdentifier)\n    {\n        if (ContainsAnyMask(fileInfoFilename))\n            return null;\n        \n        string identifier;  identifier = GetUniqueIdForFile(localFilename);\n        return fileInfoIdentifier == identifier ? null : identifier;\n    }\n\n    public static event NoParamEventHandler FileIdentifiersUpdated;\n\n    public static event LocalFileCheckProgressChangedCallback LocalFileCheckProgressChanged;\n\n    public static event NoParamEventHandler OnCustomComponentsOutdated;\n\n    public static event NoParamEventHandler OnLocalFileVersionsChecked;\n\n    public static event NoParamEventHandler OnUpdateCompleted;\n\n    public static event SetExceptionCallback OnUpdateFailed;\n\n    public static event NoParamEventHandler OnVersionStateChanged;\n\n    public static event FileDownloadCompletedEventHandler OnFileDownloadCompleted;\n\n    public static event EventHandler Restart;\n\n    public static event UpdateProgressChangedCallback UpdateProgressChanged;\n\n    public delegate void LocalFileCheckProgressChangedCallback(int checkedFileCount, int totalFileCount);\n\n    public delegate void NoParamEventHandler();\n\n    public delegate void SetExceptionCallback(Exception ex);\n\n    public delegate void UpdateProgressChangedCallback(string currFileName, int currFilePercentage, int totalPercentage);\n\n    public delegate void FileDownloadCompletedEventHandler(string archiveName);\n\n    private static void ProgressMessageHandlerOnHttpReceiveProgress(object sender, HttpProgressEventArgs e) => UpdateDownloadProgress(e.ProgressPercentage);\n\n    private static void DownloadProgressChanged(string currFileName, int currentFilePercentage, int totalPercentage) => UpdateProgressChanged?.Invoke(currFileName, currentFilePercentage, totalPercentage);\n\n    private static void DoCustomComponentsOutdatedEvent() => OnCustomComponentsOutdated?.Invoke();\n\n    private static void DoFileIdentifiersUpdatedEvent()\n    {\n        Logger.Log(\"Updater: File identifiers updated.\");\n        FileIdentifiersUpdated?.Invoke();\n    }\n\n    private static void DoOnUpdateFailed(Exception ex) => OnUpdateFailed?.Invoke(ex);\n\n    private static void DoOnVersionStateChanged() => OnVersionStateChanged?.Invoke();\n\n    private static void DoUpdateCompleted() => OnUpdateCompleted?.Invoke();\n}\n"
  },
  {
    "path": "ClientUpdater/UpdaterFileInfo.cs",
    "content": "﻿// Copyright 2022-2024 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater;\n\n/// <summary>\n///  Updater file info.\n/// </summary>\ninternal sealed record UpdaterFileInfo(string Filename, int Size)\n{\n    public string Identifier { get; set; }\n\n    public string ArchiveIdentifier { get; set; }\n\n    public int ArchiveSize { get; set; }\n\n    public bool Archived => !string.IsNullOrEmpty(ArchiveIdentifier) && ArchiveSize > 0;\n}"
  },
  {
    "path": "ClientUpdater/VersionState.cs",
    "content": "﻿// Copyright 2022-2024 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nnamespace ClientUpdater;\n\n/// <summary>\n/// Updater version state.\n/// </summary>\npublic enum VersionState\n{\n    UPTODATE,\n    MISMATCHED,\n    UNKNOWN,\n    UPDATEINPROGRESS,\n    UPDATECHECKINPROGRESS,\n    OUTDATED\n}"
  },
  {
    "path": "CommonAssemblies.txt",
    "content": "ClientUpdater.dll\nClientUpdater.pdb\nCyotek.Drawing.BitmapFont.dll\nDiscordRPC.dll\nFacepunch.Steamworks.Win64.dll\nFontStashSharp.Base.dll\nFontStashSharp.Rasterizers.StbTrueTypeSharp.dll\nFontStashSharp.TextShapers.HarfBuzz.dll\nHarfBuzzSharp.dll\nlzo.net.dll\nMicrosoft.Extensions.Configuration.Abstractions.dll\nMicrosoft.Extensions.Configuration.Binder.dll\nMicrosoft.Extensions.Configuration.CommandLine.dll\nMicrosoft.Extensions.Configuration.dll\nMicrosoft.Extensions.Configuration.EnvironmentVariables.dll\nMicrosoft.Extensions.Configuration.FileExtensions.dll\nMicrosoft.Extensions.Configuration.Json.dll\nMicrosoft.Extensions.Configuration.UserSecrets.dll\nMicrosoft.Extensions.DependencyInjection.Abstractions.dll\nMicrosoft.Extensions.DependencyInjection.dll\nMicrosoft.Extensions.Diagnostics.Abstractions.dll\nMicrosoft.Extensions.Diagnostics.dll\nMicrosoft.Extensions.FileProviders.Abstractions.dll\nMicrosoft.Extensions.FileProviders.Physical.dll\nMicrosoft.Extensions.FileSystemGlobbing.dll\nMicrosoft.Extensions.Hosting.Abstractions.dll\nMicrosoft.Extensions.Hosting.dll\nMicrosoft.Extensions.Logging.Abstractions.dll\nMicrosoft.Extensions.Logging.Configuration.dll\nMicrosoft.Extensions.Logging.Console.dll\nMicrosoft.Extensions.Logging.Debug.dll\nMicrosoft.Extensions.Logging.dll\nMicrosoft.Extensions.Logging.EventLog.dll\nMicrosoft.Extensions.Logging.EventSource.dll\nMicrosoft.Extensions.Options.ConfigurationExtensions.dll\nMicrosoft.Extensions.Options.dll\nMicrosoft.Extensions.Primitives.dll\nNewtonsoft.Json.Bson.dll\nNewtonsoft.Json.dll\nOpenMcdf.dll\nRampastring.Tools.dll\nRampastring.Tools.pdb\nRampastring.Tools.xml\nSixLabors.ImageSharp.dll\nSystem.CodeDom.dll\nStbImageSharp.dll\nStbTrueTypeSharp.dll\nsteam_api64.dll\nSystem.Net.Http.Formatting.dll\nTextCopy.dll\n"
  },
  {
    "path": "CommonAssembliesNetFx.txt",
    "content": "ClientUpdater.dll\nClientUpdater.pdb\nCyotek.Drawing.BitmapFont.dll\nDiscordRPC.dll\nFacepunch.Steamworks.Win64.dll\nFontStashSharp.Base.dll\nFontStashSharp.Rasterizers.StbTrueTypeSharp.dll\nFontStashSharp.TextShapers.HarfBuzz.dll\nHarfBuzzSharp.dll\nlibHarfBuzzSharp.dylib\nlzo.net.dll\nMicrosoft.Bcl.AsyncInterfaces.dll\nMicrosoft.Extensions.Configuration.Abstractions.dll\nMicrosoft.Extensions.Configuration.Binder.dll\nMicrosoft.Extensions.Configuration.CommandLine.dll\nMicrosoft.Extensions.Configuration.dll\nMicrosoft.Extensions.Configuration.EnvironmentVariables.dll\nMicrosoft.Extensions.Configuration.FileExtensions.dll\nMicrosoft.Extensions.Configuration.Json.dll\nMicrosoft.Extensions.Configuration.UserSecrets.dll\nMicrosoft.Extensions.DependencyInjection.Abstractions.dll\nMicrosoft.Extensions.DependencyInjection.dll\nMicrosoft.Extensions.Diagnostics.Abstractions.dll\nMicrosoft.Extensions.Diagnostics.dll\nMicrosoft.Extensions.FileProviders.Abstractions.dll\nMicrosoft.Extensions.FileProviders.Physical.dll\nMicrosoft.Extensions.FileSystemGlobbing.dll\nMicrosoft.Extensions.Hosting.Abstractions.dll\nMicrosoft.Extensions.Hosting.dll\nMicrosoft.Extensions.Logging.Abstractions.dll\nMicrosoft.Extensions.Logging.Configuration.dll\nMicrosoft.Extensions.Logging.Console.dll\nMicrosoft.Extensions.Logging.Debug.dll\nMicrosoft.Extensions.Logging.dll\nMicrosoft.Extensions.Logging.EventLog.dll\nMicrosoft.Extensions.Logging.EventSource.dll\nMicrosoft.Extensions.Options.ConfigurationExtensions.dll\nMicrosoft.Extensions.Options.dll\nMicrosoft.Extensions.Primitives.dll\nNewtonsoft.Json.Bson.dll\nNewtonsoft.Json.dll\nOpenMcdf.dll\nRampastring.Tools.dll\nRampastring.Tools.pdb\nRampastring.Tools.xml\nSixLabors.ImageSharp.dll\nStbImageSharp.dll\nStbTrueTypeSharp.dll\nsteam_api64.dll\nSystem.Buffers.dll\nSystem.CodeDom.dll\nSystem.Diagnostics.DiagnosticSource.dll\nSystem.IO.FileSystem.AccessControl.dll\nSystem.Memory.dll\nSystem.Net.Http.Formatting.dll\nSystem.Numerics.Vectors.dll\nSystem.Runtime.CompilerServices.Unsafe.dll\nSystem.Security.AccessControl.dll\nSystem.Security.Principal.Windows.dll\nSystem.Text.Encoding.CodePages.dll\nSystem.Text.Encodings.Web.dll\nSystem.Text.Json.dll\nSystem.Threading.Tasks.Extensions.dll\nSystem.ValueTuple.dll\nTextCopy.dll\n"
  },
  {
    "path": "Contributing.md",
    "content": "# Contributing\n\nThis file lists the contributing guidelines that are used in the project.\n\n### Commit style guide\n\nCommits start with a capital letter and don't end in a punctuation mark.\n\nRight:\n```\nTreat usernames as case-insensitive in user collections\n```\n\nWrong:\n```\ntreat usernames as case-insensitive in user collections.\n```\n\nUse imperative present tense in commit messages instead of past tense.\n\nRight:\n```\nAdd null-check for GameMode\n```\n\nWrong:\n```\nAdded null-check for GameMode\n```\n\n### Pull Requests\n\nMake 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.\n\nOne pull request should _only implement one feature_ or _fix one bug_, unless there is a good reason for grouping the changes together.\n\nDo 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.\n\nIf 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.\n\n**Make sure your code and commits match this style guide before you create your pull request.**\n\nPull 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.\n\n### Code style guide\n\nWe have established a couple of code style rules to keep things consistent. Please check your code style before committing the code.\n- We use spaces instead of tabs to indent code.\n- 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:\n```cs\nif (SomeReallyLongCondition() ||\n    ThatSplitsIntoMultipleLines())\n{\n    DoSomethingHere();\n    DoSomethingMore();\n}\n```\n- 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:\n```cs\n// OK\nif (Something())\n    DoSomething();\n\n// OK\nif (SomeReallyLongCondition() ||\n    ThatSplitsIntoMultipleLines())\n{\n    DoSomething();\n}\n\n// OK\nif (SomeCondition())\n{\n    if (SomeOtherCondition())\n        DoSomething();\n}\n\n// OK\nif (SomeCondition())\n{\n    return VeryLongExpression()\n        || ThatSplitsIntoMultipleLines();\n}\n```\n- Only empty curly brace blocks may be left on the same line for both opening and closing braces (if appropriate).\n- If you use `if`-`else` you should either have all of the code blocks braced or braceless to keep things consistent.\n- 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:\n  - `return` statements (except when there is only one line of code except that statement);\n  - 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);\n  - code blocks (braceless or not) or anything using code blocks (function or hook definitions, classes, namespaces etc.)\n```cs\n// OK\nint localVar = Something();\nif (SomeConditionUsing(localVar))\n    ...\n\n// OK\nint localVar = Something();\nint anotherLocalVar = OtherSomething();\n\nif (SomeConditionUsing(localVar, anotherLocalVar))\n    ...\n\n// OK\nint localVar = Something();\n\nif (SomeConditionUsing(localVar))\n    ...\n\nif (SomeOtherConditionUsing(localVar))\n    ...\n\nlocalVar = OtherSomething();\n\n// OK\nif (SomeCondition())\n{\n    Code();\n    OtherCode();\n\n    return;\n}\n\n// OK\nif (SomeCondition())\n{\n    SmallCode();\n    return;\n}\n```\n- 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.\n- A space must be put between braces of empty curly brace blocks.\n```cs\n// OK\nvar list = new List<int>();\n\n// Not OK\nvar something = 6;\n```\n- Local variables, function/method args and private class fields are named in `camelCase` and a descriptive name, like `ircUser` for a local `IrcUser` variable.\n- Classes, namespaces, and properties are always written in `PascalCase`.\n- Class fields that can be set via INI tags should be named exactly like ini tags with dots replaced with underscores.\n\n#### Formatter requirements\n\n- 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.\n- 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.\n\n- 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:\n    - You have reached the threshold for running the code formatter on the whole file, or\n    - You have added and/or removed `using` directives, especially if you have added AND removed `using` directives.\n\n#### C# nullability requirements\n\nThe project has mixed usages of nullability annotations. \n\n- 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.\n- 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.\n\n### Forbidden APIs\n\n- You should not use `BitConverter`, because its behavior depends on platform endianness via `BitConverter.IsLittleEndian`. Instead, you should use `BinaryPrimitives` for byte conversions.\n\n### Text encoding\n- Before converting between byte arrays and strings, you should always think carefully about the encoding to be used.\n    - For client-side text, you should use UTF-8 encoding without BOM, unless you have a good reason not to.\n    - 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.\n        - 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.\n\n### Literal strings\n- 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.\n\n- 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:\n    - Do not localize non-literal strings.\n    ```cs\n    // OK\n    string greetingText = string.Format(\"Hello, {0}!\".L10N(\"Client:Main:GreetingMessage\"), userName);\n\n    // Not OK\n    string greetingText = string.Format(\"Hello, {0}!\", userName).L10N(\"Client:Main:GreetingMessage\");\n\n    // Not OK\n    string greetingText = $\"Hello, {userName}!\";\n\n    // Not OK\n    string greetingText = $\"Hello, {userName}!\".L10N(\"Client:Main:GreetingMessage\");\n    ```\n    - Do not conditionally determine the key or the literal string.\n    ```cs\n    // OK\n    bool isSuccess = DoSomething();\n    string message = isSuccess\n        ? \"Operation succeeded.\".L10N(\"Client:Main:OperationSucceededMessage\")\n        : \"Operation failed.\".L10N(\"Client:Main:OperationFailedMessage\");\n\n    // Not OK\n    bool isSuccess = DoSomething();\n    string message = (isSuccess ? \"Operation succeeded.\" : \"Operation failed.\").L10N(isSuccess ? \"Client:Main:OperationSucceededMessage\" : \"Client:Main:OperationFailedMessage\");\n\n    // OK\n    int resultErrorCode = DoSomething();\n    string errorMessage = resultErrorCode switch\n    {\n        0 => \"Operation succeeded.\".L10N(\"Client:Main:OperationSucceededMessage\"),\n        1 => \"Operation failed.\".L10N(\"Client:Main:OperationFailedMessage\"),\n        2 => \"Operation failed due to file not found.\".L10N(\"Client:Main:FileNotFoundMessage\"),\n        _ => \"Operation failed due to unknown error.\".L10N(\"Client:Main:UnknownErrorMessage\")\n    };\n\n    // Not OK\n    int resultErrorCode = DoSomething();\n    string errorMessage = resultErrorCode switch\n    {\n        0 => \"Operation succeeded.\",\n        1 => \"Operation failed.\",\n        2 => \"Operation failed due to file not found.\",\n        _ => \"Operation failed due to unknown error.\"\n    }.L10N($\"Client:Main:ResultErrorCode{resultErrorCode}Message\");\n    ```\n\n- 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.\n```cs\n// OK\nclass MyClass\n{\n    public static string GreetingMessage => \"Hello, world!\".L10N(\"Client:MyClass:GreetingMessage\");\n}\n\n// Not OK\nclass MyClass\n{\n    public static string GreetingMessage = \"Hello, world!\".L10N(\"Client:MyClass:GreetingMessage\");\n}\n```\n- 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.\n```cs\n// OK\nstring message = \"An error occurred. Error:\".L10N(\"Client:Main:ErrorMessage\") + \" \" + errorDetails;\n\n// Not OK\nstring message = \"An error occurred. Error: \".L10N(\"Client:Main:ErrorMessage\") + errorDetails;\n\n// Not OK\nstring message = \"An error occurred. Error:\".L10N(\"Client:Main:ErrorMessage\") + errorDetails; // This violates the English punctuation rules\n```\n\nNote: This guide is not exhaustive and may be adjusted in the future.\n"
  },
  {
    "path": "DXClient.slnx",
    "content": "<Solution>\n  <Configurations>\n    <BuildType Name=\"UniversalGLDebug\" />\n    <BuildType Name=\"UniversalGLRelease\" />\n    <BuildType Name=\"WindowsDXDebug\" />\n    <BuildType Name=\"WindowsDXRelease\" />\n    <BuildType Name=\"WindowsGLDebug\" />\n    <BuildType Name=\"WindowsGLRelease\" />\n    <BuildType Name=\"WindowsXNADebug\" />\n    <BuildType Name=\"WindowsXNARelease\" />\n    <Platform Name=\"Any CPU\" />\n    <Platform Name=\"x86\" />\n  </Configurations>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\".gitattributes\" />\n    <File Path=\".gitignore\" />\n    <File Path=\"Directory.Build.props\" />\n    <File Path=\"Directory.Build.targets\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"GitVersion.yml\" />\n    <File Path=\"global.json\" />\n    <File Path=\"NuGet.config\" />\n  </Folder>\n  <Project Path=\"ClientCore/ClientCore.csproj\" />\n  <Project Path=\"ClientGUI/ClientGUI.csproj\">\n    <Platform Solution=\"WindowsXNADebug|*\" Project=\"x86\" />\n    <Platform Solution=\"WindowsXNARelease|*\" Project=\"x86\" />\n  </Project>\n  <Project Path=\"ClientUpdater/ClientUpdater.csproj\" />\n  <Project Path=\"DXMainClient/DXMainClient.csproj\">\n    <Platform Solution=\"WindowsXNADebug|*\" Project=\"x86\" />\n    <Platform Solution=\"WindowsXNARelease|*\" Project=\"x86\" />\n  </Project>\n  <Project Path=\"Rampastring.XNAUI/Rampastring.Tools/Rampastring.Tools.csproj\">\n    <BuildType Solution=\"UniversalGLDebug|*\" Project=\"Debug\" />\n    <BuildType Solution=\"UniversalGLRelease|*\" Project=\"Release\" />\n    <BuildType Solution=\"WindowsDXDebug|*\" Project=\"Debug\" />\n    <BuildType Solution=\"WindowsDXRelease|*\" Project=\"Release\" />\n    <BuildType Solution=\"WindowsGLDebug|*\" Project=\"Debug\" />\n    <BuildType Solution=\"WindowsGLRelease|*\" Project=\"Release\" />\n    <BuildType Solution=\"WindowsXNADebug|*\" Project=\"Debug\" />\n    <BuildType Solution=\"WindowsXNARelease|*\" Project=\"Release\" />\n  </Project>\n  <Project Path=\"Rampastring.XNAUI/Rampastring.XNAUI.csproj\" />\n  <Project Path=\"SecondStageUpdater/SecondStageUpdater.csproj\" />\n  <Project Path=\"TranslationNotifierGenerator/TranslationNotifierGenerator.csproj\" />\n</Solution>\n"
  },
  {
    "path": "DXMainClient/AdminRestarter.cs",
    "content": "#nullable enable\nusing System;\nusing System.Diagnostics;\nusing System.Runtime.Versioning;\nusing System.Security.Principal;\n\nusing Rampastring.Tools;\nusing ClientCore;\n\nnamespace DTAClient\n{\n    /// <summary>\n    /// Utility for restarting the client with administrator privileges.\n    /// </summary>\n    [SupportedOSPlatform(\"windows\")]\n    public static class AdminRestarter\n    {\n        /// <summary>\n        /// Checks if the application is running with administrator privileges.\n        /// </summary>\n        /// <returns>True if running as administrator, false otherwise.</returns>\n        public static bool IsRunningAsAdministrator()\n        {\n            try\n            {\n                using WindowsIdentity identity = WindowsIdentity.GetCurrent();\n                WindowsPrincipal principal = new WindowsPrincipal(identity);\n                return principal.IsInRole(WindowsBuiltInRole.Administrator);\n            }\n            catch\n            {\n                return false;\n            }\n        }\n\n        /// <summary>\n        /// Restarts the current application with administrator privileges.\n        /// </summary>\n        /// <returns>True if the restart was initiated successfully, false otherwise.</returns>\n        public static bool RestartAsAdmin()\n        {\n            bool runNativeWindowsExe = true;\n#if !NETFRAMEWORK\n            runNativeWindowsExe = false;\n#endif\n\n            try\n            {\n                if (runNativeWindowsExe)\n                {\n                    using var _ = Process.Start(new ProcessStartInfo\n                    {\n                        FileName = SafePath.CombineFilePath(ProgramConstants.StartupExecutable),\n                        Verb = \"runas\",\n                        UseShellExecute = true,\n                    });\n                }\n                else\n                {\n                    // Calling dotnet.exe has the following disadvantages:\n                    // 1. We need to specify `UseShellExecute = true` for the `Runas` verb, which means we cannot hide the console window despite setting `CreateNoWindow = true`.\n                    // 2. For XNA build, we need to call the x86 version of dotnet.exe.\n\n                    // 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.\n\n                    string arguments;\n#if XNA\n                    arguments = \"-NET8 -XNA\";\n#elif DX\n                    arguments = \"-NET8 -DX\";\n#elif GL\n                    // Note: we can assume no UGL build here because this class is labeled as Windows-only.\n                    arguments = \"-NET8 -OGL\";\n#else\n#error Unknown build configuration\n#endif\n\n                    using var _ = Process.Start(new ProcessStartInfo\n                    {\n                        FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.LauncherExe),\n                        Verb = \"runas\",\n                        Arguments = arguments,\n                        UseShellExecute = true,\n                    });\n                }\n\n                return true;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Failed to restart with admin privileges: \" + ex.ToString());\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Campaign/CampaignCheckBox.cs",
    "content": "using System;\n\nusing ClientCore;\n\nusing DTAClient.DXGUI.Generic;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Campaign;\n\npublic class CampaignCheckBox : GameSessionCheckBox\n{\n    public CampaignCheckBox(WindowManager windowManager) : base (windowManager) { }\n    \n    public bool ResetToDefaultOnGameExit { get; private set; }\n\n    public override void Initialize()\n    {\n        // Find the campaign selector that this control belongs to and register ourselves as a game option.\n\n        XNAControl parent = Parent;\n        while (true)\n        {\n            if (parent == null)\n                break;\n\n            // oh no, we have a circular class reference here!\n            if (parent is CampaignSelector configView)\n            {\n                configView.CheckBoxes.Add(this);\n                break;\n            }\n\n            parent = parent.Parent;\n        }\n\n        base.Initialize();\n    }\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        switch (key)\n        {\n            case \"ResetToDefaultOnGameExit\":\n                ResetToDefaultOnGameExit = Conversions.BooleanFromString(value, false);\n                return;\n\n            case \"CustomIniPath\" when !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI:\n                throw new Exception($\"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\\n\\n\"\n                    + $\"Offending setting control: {Name}\");\n        }\n        \n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Campaign/CampaignDropDown.cs",
    "content": "using System;\n\nusing ClientCore;\n\nusing DTAClient.DXGUI.Generic;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Campaign;\n\npublic class CampaignDropDown : GameSessionDropDown\n{\n    public CampaignDropDown(WindowManager windowManager) : base (windowManager) { }\n    \n    public override void Initialize()\n    {\n        // Find the campaign selector that this control belongs to and register ourselves as a game option.\n\n        XNAControl parent = Parent;\n        while (true)\n        {\n            if (parent == null)\n                break;\n\n            // oh no, we have a circular class reference here!\n            if (parent is CampaignSelector configView)\n            {\n                configView.DropDowns.Add(this);\n                break;\n            }\n\n            parent = parent.Parent;\n        }\n\n        base.Initialize();\n    }\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        if (key == \"DataWriteMode\" && value.ToUpper() == \"MAPCODE\" && !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)\n        {\n            throw new Exception($\"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\\n\\n\"\n                + $\"Offending setting control: {Name}\");\n        }\n        \n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Campaign/CampaignSelector.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\nusing ClientGUI.Settings;\n\nusing ClientUpdater;\n\nusing DTAClient.Domain;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Campaign\n{\n    public class CampaignSelector : XNAWindow\n    {\n        private const int DEFAULT_WIDTH = 650;\n        private const int DEFAULT_HEIGHT = 600;\n\n        private const string SETTINGS_PATH = \"Client/CampaignSettings.ini\";\n\n        private static string[] DifficultyNames = new string[] { \"Easy\", \"Medium\", \"Hard\" };\n\n        private static string[] DifficultyIniPaths = new string[]\n        {\n            \"INI/Map Code/Difficulty Easy.ini\",\n            \"INI/Map Code/Difficulty Medium.ini\",\n            \"INI/Map Code/Difficulty Hard.ini\"\n        };\n\n        public CampaignSelector(WindowManager windowManager, DiscordHandler discordHandler, CampaignTagSelector campaignTagSelector) : base(windowManager)\n        {\n            this.discordHandler = discordHandler;\n            this.campaignTagSelector = campaignTagSelector;\n        }\n\n        private DiscordHandler discordHandler;\n        private CampaignTagSelector campaignTagSelector;\n\n        private List<Mission> selectedMissions = [];\n\n        private XNAPanel pnlMissionPreview;\n        private bool pnlMissionPreviewBackgroundTextureNeedsDispose = false;\n        private string missionPreviewFolder => SafePath.CombineDirectoryPath(ProgramConstants.GetBaseResourcePath(), \"Mission Previews\");\n        private string defaultMissionPreviewPath => SafePath.CombineFilePath(missionPreviewFolder, \"Default.png\");\n        private bool pnlMissionPreviewEnabled => File.Exists(defaultMissionPreviewPath);\n\n        private XNAListBox lbCampaignList;\n        private XNAClientButton btnLaunch;\n        private XNAClientButton btnCancel;\n        private XNAClientButton btnReturn;\n        private XNATextBlock tbMissionDescription;\n        private XNATrackbar trbDifficultySelector;\n        private List<IUserSetting> userSettings = new List<IUserSetting>();\n\n        private CheaterWindow cheaterWindow;\n\n        public List<CampaignCheckBox> CheckBoxes { get; } = new();\n        public List<CampaignDropDown> DropDowns { get; } = new();\n\n        private IniFile gameOptionsIni;\n\n        private string[] filesToCheck = new string[]\n        {\n            \"INI/AI.ini\",\n            \"INI/AIE.ini\",\n            \"INI/Art.ini\",\n            \"INI/ArtE.ini\",\n            \"INI/Enhance.ini\",\n            \"INI/Rules.ini\",\n            \"INI/Map Code/Difficulty Hard.ini\",\n            \"INI/Map Code/Difficulty Medium.ini\",\n            \"INI/Map Code/Difficulty Easy.ini\"\n        };\n\n        private Mission missionToLaunch;\n\n        private List<Mission> _allMissions = [];\n        public IReadOnlyCollection<Mission> AllMissions { get => _allMissions; }\n\n        private Dictionary<int, Mission> _uniqueIDToMissions = new();\n        public IReadOnlyDictionary<int, Mission> UniqueIDToMissions => _uniqueIDToMissions;\n\n        private void AddMission(Mission mission)\n        {\n            // no matter whether the key is duplicated, the mission is always added to AllMissions\n            _allMissions.Add(mission);\n\n            // but only the first mission is recorded in UniqueIDToMissions\n            if (_uniqueIDToMissions.ContainsKey(mission.CustomMissionID))\n            {\n                Logger.Log($\"CampaignSelector: duplicated mission. CodeName: {mission.CodeName}. ID: {mission.CustomMissionID}. Description: {mission.UntranslatedGUIName}.\");\n                if (!string.IsNullOrEmpty(mission.Scenario))\n                    mission.Enabled = false;\n            }\n            else\n            {\n                _uniqueIDToMissions.Add(mission.CustomMissionID, mission);\n            }\n        }\n\n        public override void Initialize()\n        {\n            Name = \"CampaignSelector\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"missionselectorbg.png\");\n            ClientRectangle = new Rectangle(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);\n            BorderColor = UISettings.ActiveSettings.PanelBorderColor;\n\n            gameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(),\n                ClientConfiguration.GAME_OPTIONS));\n\n            var lblSelectCampaign = new XNALabel(WindowManager);\n            lblSelectCampaign.Name = nameof(lblSelectCampaign);\n            lblSelectCampaign.FontIndex = 1;\n            lblSelectCampaign.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            lblSelectCampaign.Text = \"MISSIONS:\".L10N(\"Client:Main:Missions\");\n\n            lbCampaignList = new XNAListBox(WindowManager);\n            lbCampaignList.Name = nameof(lbCampaignList);\n            lbCampaignList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2);\n            lbCampaignList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbCampaignList.ClientRectangle = new Rectangle(12,\n                lblSelectCampaign.Bottom + 6, 300, 516);\n            lbCampaignList.SelectedIndexChanged += LbCampaignList_SelectedIndexChanged;\n\n            var lblMissionDescriptionHeader = new XNALabel(WindowManager);\n            lblMissionDescriptionHeader.Name = nameof(lblMissionDescriptionHeader);\n            lblMissionDescriptionHeader.FontIndex = 1;\n            lblMissionDescriptionHeader.ClientRectangle = new Rectangle(\n                lbCampaignList.Right + 12,\n                lblSelectCampaign.Y, 0, 0);\n            lblMissionDescriptionHeader.Text = \"MISSION DESCRIPTION:\".L10N(\"Client:Main:MissionDescription\");\n\n            tbMissionDescription = new XNATextBlock(WindowManager);\n            tbMissionDescription.Name = nameof(tbMissionDescription);\n            tbMissionDescription.ClientRectangle = new Rectangle(\n                lblMissionDescriptionHeader.X,\n                lblMissionDescriptionHeader.Bottom + 6,\n                Width - 24 - lbCampaignList.Right,\n                pnlMissionPreviewEnabled ? 430 - 200 - 12 : 430);\n            tbMissionDescription.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            tbMissionDescription.Alpha = 1.0f;\n            tbMissionDescription.BackgroundTexture = AssetLoader.CreateTexture(AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor),\n                tbMissionDescription.Width, tbMissionDescription.Height);\n\n            var lblDifficultyLevel = new XNALabel(WindowManager);\n            lblDifficultyLevel.Name = nameof(lblDifficultyLevel);\n            lblDifficultyLevel.Text = \"DIFFICULTY LEVEL\".L10N(\"Client:Main:DifficultyLevel\");\n            lblDifficultyLevel.FontIndex = 1;\n            Vector2 textSize = Renderer.GetTextDimensions(lblDifficultyLevel.Text, lblDifficultyLevel.FontIndex);\n            lblDifficultyLevel.ClientRectangle = new Rectangle(\n                tbMissionDescription.X + (tbMissionDescription.Width - (int)textSize.X) / 2,\n                tbMissionDescription.Bottom + 12, (int)textSize.X, (int)textSize.Y);\n\n            trbDifficultySelector = new XNATrackbar(WindowManager);\n            trbDifficultySelector.Name = nameof(trbDifficultySelector);\n            trbDifficultySelector.ClientRectangle = new Rectangle(\n                tbMissionDescription.X, lblDifficultyLevel.Bottom + 6,\n                tbMissionDescription.Width, 30);\n            trbDifficultySelector.MinValue = 0;\n            trbDifficultySelector.MaxValue = 2;\n            trbDifficultySelector.BackgroundTexture = AssetLoader.CreateTexture(\n                new Color(0, 0, 0, 128), 2, 2);\n            trbDifficultySelector.ButtonTexture = AssetLoader.LoadTextureUncached(\n                \"trackbarButton_difficulty.png\");\n\n            var lblEasy = new XNALabel(WindowManager);\n            lblEasy.Name = nameof(lblEasy);\n            lblEasy.FontIndex = 1;\n            lblEasy.Text = \"EASY\".L10N(\"Client:Main:DifficultyEasy\");\n            lblEasy.ClientRectangle = new Rectangle(trbDifficultySelector.X,\n                trbDifficultySelector.Bottom + 6, 1, 1);\n\n            var lblNormal = new XNALabel(WindowManager);\n            lblNormal.Name = nameof(lblNormal);\n            lblNormal.FontIndex = 1;\n            lblNormal.Text = \"NORMAL\".L10N(\"Client:Main:DifficultyNormal\");\n            textSize = Renderer.GetTextDimensions(lblNormal.Text, lblNormal.FontIndex);\n            lblNormal.ClientRectangle = new Rectangle(\n                tbMissionDescription.X + (tbMissionDescription.Width - (int)textSize.X) / 2,\n                lblEasy.Y, (int)textSize.X, (int)textSize.Y);\n\n            var lblHard = new XNALabel(WindowManager);\n            lblHard.Name = nameof(lblHard);\n            lblHard.FontIndex = 1;\n            lblHard.Text = \"HARD\".L10N(\"Client:Main:DifficultyHard\");\n            lblHard.ClientRectangle = new Rectangle(\n                tbMissionDescription.Right - lblHard.Width,\n                lblEasy.Y, 1, 1);\n\n            btnLaunch = new XNAClientButton(WindowManager);\n            btnLaunch.Name = nameof(btnLaunch);\n            btnLaunch.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLaunch.Text = \"Launch\".L10N(\"Client:Main:ButtonLaunch\");\n            btnLaunch.AllowClick = false;\n            btnLaunch.LeftClick += BtnLaunch_LeftClick;\n\n            btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.ClientRectangle = new Rectangle(Width - 145,\n                btnLaunch.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            if (pnlMissionPreviewEnabled)\n            {\n                pnlMissionPreview = new XNAPanel(WindowManager);\n                pnlMissionPreview.Name = nameof(pnlMissionPreview);\n                pnlMissionPreview.ClientRectangle = new Rectangle(\n                    tbMissionDescription.X,\n                    tbMissionDescription.Bottom + 12,\n                    tbMissionDescription.Width,\n                    200);\n\n                pnlMissionPreview.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n                pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture(AssetLoader.LoadTextureUncached(defaultMissionPreviewPath), pnlMissionPreview.Width, pnlMissionPreview.Height);\n                pnlMissionPreviewBackgroundTextureNeedsDispose = true;\n            }\n            else\n            {\n                pnlMissionPreview = null;\n            }\n\n            AddChild(lblSelectCampaign);\n            AddChild(lblMissionDescriptionHeader);\n            AddChild(lbCampaignList);\n            AddChild(tbMissionDescription);\n            AddChild(lblDifficultyLevel);\n            AddChild(btnLaunch);\n            AddChild(btnCancel);\n            AddChild(trbDifficultySelector);\n            AddChild(lblEasy);\n            AddChild(lblNormal);\n            AddChild(lblHard);\n\n            if (pnlMissionPreview != null)\n                AddChild(pnlMissionPreview);\n\n            if (ClientConfiguration.Instance.CampaignTagSelectorEnabled)\n            {\n                btnReturn = new XNAClientButton(WindowManager);\n                btnReturn.Name = nameof(btnReturn);\n                btnReturn.ClientRectangle = new Rectangle(trbDifficultySelector.X,\n                btnLaunch.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n                btnReturn.Text = \"Campaigns\".L10N(\"Client:Main:ButtonReturnToCampaigns\");\n                btnReturn.LeftClick += BtnReturn_LeftClick;\n                btnReturn.Disable();\n                AddChild(btnReturn);\n            }\n\n            // Set control attributes from INI file\n            base.Initialize();\n\n            // Center on screen\n            CenterOnParent();\n\n            trbDifficultySelector.Value = UserINISettings.Instance.Difficulty;\n\n            userSettings.AddRange(Children.OfType<IUserSetting>());\n\n            ReadMissionList();\n\n            cheaterWindow = new CheaterWindow(WindowManager);\n            var dp = new DarkeningPanel(WindowManager);\n            dp.AddChild(cheaterWindow);\n            AddChild(dp);\n            dp.CenterOnParent();\n            cheaterWindow.CenterOnParent();\n            cheaterWindow.YesClicked += CheaterWindow_YesClicked;\n            cheaterWindow.Disable();\n\n            LoadSettings();\n        }\n\n        private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (lbCampaignList.SelectedIndex == -1)\n            {\n                tbMissionDescription.Text = string.Empty;\n\n                UpdateMissionPreview(string.Empty);\n\n                btnLaunch.AllowClick = false;\n                return;\n            }\n\n            Mission mission = selectedMissions[lbCampaignList.SelectedIndex];\n\n            UpdateMissionPreview(mission.PreviewImage);\n\n            if (string.IsNullOrEmpty(mission.Scenario))\n            {\n                tbMissionDescription.Text = string.Empty;\n                btnLaunch.AllowClick = false;\n                return;\n            }\n\n            tbMissionDescription.Text = mission.GUIDescription;\n\n            if (!mission.Enabled)\n            {\n                btnLaunch.AllowClick = false;\n                return;\n            }\n\n            btnLaunch.AllowClick = true;\n        }\n\n        // TODO: Modify XNAUI by adding PanelBackgroundImageDrawMode.LETTERBOXED as a new draw mode.\n        private Texture2D CreateLetterboxedTexture(Texture2D sourceTexture, int targetWidth, int targetHeight, bool disposeSourceTexture = true)\n        {\n            // Calculate aspect ratios\n            float sourceAspect = (float)sourceTexture.Width / sourceTexture.Height;\n            float targetAspect = (float)targetWidth / targetHeight;\n\n            int drawWidth, drawHeight, drawX, drawY;\n\n            // Determine scaled dimensions while maintaining aspect ratio\n            if (sourceAspect > targetAspect)\n            {\n                // Source is wider - fit to width\n                drawWidth = targetWidth;\n                drawHeight = (int)(targetWidth / sourceAspect);\n                drawX = 0;\n                drawY = (targetHeight - drawHeight) / 2;\n            }\n            else\n            {\n                // Source is taller - fit to height\n                drawHeight = targetHeight;\n                drawWidth = (int)(targetHeight * sourceAspect);\n                drawX = (targetWidth - drawWidth) / 2;\n                drawY = 0;\n            }\n\n            // Create the composite texture\n            RenderTarget2D renderTarget = new RenderTarget2D(\n                WindowManager.GraphicsDevice,\n                targetWidth,\n                targetHeight,\n                false,\n                SurfaceFormat.Color,\n                DepthFormat.None);\n\n            WindowManager.GraphicsDevice.SetRenderTarget(renderTarget);\n            WindowManager.GraphicsDevice.Clear(AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor));\n\n            var spriteBatch = new SpriteBatch(WindowManager.GraphicsDevice);\n            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);\n            spriteBatch.Draw(\n                sourceTexture,\n                new Rectangle(drawX, drawY, drawWidth, drawHeight),\n                Color.White);\n            spriteBatch.End();\n            spriteBatch.Dispose();\n\n            WindowManager.GraphicsDevice.SetRenderTarget(null);\n\n            if (disposeSourceTexture)\n                sourceTexture.Dispose();\n\n            return renderTarget;\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            SaveSettings();\n            Disable();\n        }\n\n        private void BtnReturn_LeftClick(object sender, EventArgs e)\n        {\n            campaignTagSelector.NoFadeSwitch();\n        }\n\n        private void BtnLaunch_LeftClick(object sender, EventArgs e)\n        {\n            SaveSettings();\n\n            int selectedMissionId = lbCampaignList.SelectedIndex;\n\n            Mission mission = selectedMissions[selectedMissionId];\n\n            if (!ClientConfiguration.Instance.ModMode &&\n                (!Updater.IsFileNonexistantOrOriginal(mission.Scenario) || AreFilesModified()))\n            {\n                // Confront the user by showing the cheater screen\n                missionToLaunch = mission;\n                cheaterWindow.Enable();\n                return;\n            }\n\n            LaunchMission(mission);\n        }\n\n        private bool AreFilesModified()\n        {\n            foreach (string filePath in filesToCheck)\n            {\n                if (!Updater.IsFileNonexistantOrOriginal(filePath))\n                    return true;\n            }\n\n            return false;\n        }\n\n        /// <summary>\n        /// Called when the user wants to proceed to the mission despite having\n        /// being called a cheater.\n        /// </summary>\n        private void CheaterWindow_YesClicked(object sender, EventArgs e)\n        {\n            LaunchMission(missionToLaunch);\n        }\n\n        /// <summary>\n        /// Starts a singleplayer mission.\n        /// </summary>\n        private void LaunchMission(Mission mission)\n        {\n            CustomMissionHelper.CopySupplementalMissionFiles(mission);\n\n            string scenario = mission.Scenario;\n\n            FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS);\n\n            spawnerSettingsFile.Delete();\n\n            bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI;\n\n            Logger.Log(\"About to write spawn.ini.\");\n            IniFile spawnIni = new(spawnerSettingsFile.FullName)\n            {\n                Comment = \"Generated by CnCNet Client\"\n            };\n            IniSection spawnIniSettings = new(\"Settings\");\n\n            if (copyMapsToSpawnmapINI)\n                spawnIniSettings.AddKey(\"Scenario\", \"spawnmap.ini\");\n            else\n                spawnIniSettings.AddKey(\"Scenario\", scenario);\n\n            // No one wants to play missions on Fastest, so we'll change it to Faster\n            if (UserINISettings.Instance.GameSpeed == 0)\n                UserINISettings.Instance.GameSpeed.Value = 1;\n\n            spawnIniSettings.AddKey(\"CampaignID\", mission.CampaignID.ToString(CultureInfo.InvariantCulture));\n            spawnIniSettings.AddKey(\"GameSpeed\", UserINISettings.Instance.GameSpeed.ToString());\n\n            switch (ClientConfiguration.Instance.ClientGameType)\n            {\n                case ClientType.YR or ClientType.Ares:\n                    spawnIniSettings.AddKey(\"Ra2Mode\", (!mission.RequiredAddon).ToString(CultureInfo.InvariantCulture));\n                    break;\n                case ClientType.TS:\n                    spawnIniSettings.AddKey(\"Firestorm\", mission.RequiredAddon.ToString(CultureInfo.InvariantCulture));\n                    break;\n                // TODO figure out the RA one\n            }\n\n            spawnIniSettings.AddKey(\"CustomLoadScreen\", LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));\n\n            spawnIniSettings.AddKey(\"IsSinglePlayer\", \"Yes\");\n            spawnIniSettings.AddKey(\"SidebarHack\", ClientConfiguration.Instance.SidebarHack.ToString(CultureInfo.InvariantCulture));\n            spawnIniSettings.AddKey(\"Side\", mission.Side.ToString(CultureInfo.InvariantCulture));\n            spawnIniSettings.AddKey(\"BuildOffAlly\", mission.BuildOffAlly.ToString(CultureInfo.InvariantCulture));\n\n            UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;\n\n            spawnIniSettings.AddKey(\"DifficultyModeHuman\", mission.PlayerAlwaysOnNormalDifficulty ? \"1\" : trbDifficultySelector.Value.ToString(CultureInfo.InvariantCulture));\n            spawnIniSettings.AddKey(\"DifficultyModeComputer\", GetComputerDifficulty().ToString(CultureInfo.InvariantCulture));\n\n            if (mission.IsCustomMission)\n            {\n                spawnIniSettings.AddKey(\"CustomMissionID\", mission.CustomMissionID.ToString(CultureInfo.InvariantCulture));\n            }\n\n            spawnIni.AddSection(spawnIniSettings);\n            WriteMissionSectionToSpawnIni(spawnIni, mission);\n\n            foreach (CampaignCheckBox chkBox in CheckBoxes)\n                chkBox.ApplySpawnIniCode(spawnIni);\n\n            foreach (CampaignDropDown dd in DropDowns)\n                dd.ApplySpawnIniCode(spawnIni);\n\n            // Apply forced options from GameOptions.ini\n\n            List<string> forcedKeys = gameOptionsIni.GetSectionKeys(\"CampaignForcedSpawnIniOptions\");\n\n            if (forcedKeys != null)\n            {\n                foreach (string key in forcedKeys)\n                {\n                    spawnIni.SetStringValue(\"Settings\", key,\n                        gameOptionsIni.GetStringValue(\"CampaignForcedSpawnIniOptions\", key, String.Empty));\n                }\n            }\n\n            spawnIni.WriteIniFile();\n\n            var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value]));\n            string difficultyName = DifficultyNames[trbDifficultySelector.Value];\n\n            if (copyMapsToSpawnmapINI)\n            {\n                var mapIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario));\n\n                IniFile.ConsolidateIniFiles(mapIni, difficultyIni);\n\n                foreach (CampaignCheckBox chkBox in CheckBoxes)\n                    chkBox.ApplyMapCode(mapIni, gameMode: null);\n\n                foreach (CampaignDropDown dd in DropDowns)\n                    dd.ApplyMapCode(mapIni, gameMode: null);\n\n                mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"spawnmap.ini\"));\n            }\n\n            UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;\n            UserINISettings.Instance.SaveSettings();\n\n            if (ClientConfiguration.Instance.ReturnToMainMenuOnMissionLaunch)\n                Disable();\n            else\n                ToggleControls(false);\n\n            discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true);\n            GameProcessLogic.GameProcessExited += GameProcessExited_Callback;\n\n            GameProcessLogic.StartGameProcess(WindowManager);\n        }\n\n        public static void WriteMissionSectionToSpawnIni(IniFile spawnIni, Mission mission)\n        {\n            bool hasGameMissionData = false;\n            string scenarioPath = SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario);\n\n            if (!mission.IsCustomMission && File.Exists(scenarioPath))\n            {\n                var mapIni = new IniFile(scenarioPath);\n                mission.GameMissionConfigSection = mapIni.GetSection(\"GameMissionConfig\");\n\n                if (mission.GameMissionConfigSection is not null)\n                    hasGameMissionData = true;\n            }\n\n            if (mission.IsCustomMission && mission.GameMissionConfigSection is not null || hasGameMissionData)\n            {\n                // copy an IniSection\n                IniSection spawnIniMissionIniSection = new(mission.Scenario);\n                string loadingScreenName = string.Empty;\n                string loadingScreenPalName = string.Empty;\n                foreach (var kvp in mission.GameMissionConfigSection.Keys)\n                {\n                    if (string.IsNullOrEmpty(kvp.Value))\n                    {\n                        if (kvp.Key.Equals(\"LS640BkgdName\", StringComparison.InvariantCulture) || kvp.Key.Equals(\"LS800BkgdName\", StringComparison.InvariantCulture))\n                            loadingScreenName = kvp.Value;\n                        else if (kvp.Key.Equals(\"LS800BkgdPal\", StringComparison.InvariantCulture))\n                            loadingScreenPalName = kvp.Value;\n                    }\n\n                    spawnIniMissionIniSection.AddKey(kvp.Key, kvp.Value);\n                }\n\n                if (string.IsNullOrEmpty(loadingScreenName))\n                {\n                    string lsFilename = CustomMissionHelper.CustomMissionSupplementDefinition.FirstOrDefault(x => x.extension.Equals(\"shp\", StringComparison.InvariantCultureIgnoreCase)).filename;\n\n                    if (!string.IsNullOrEmpty(lsFilename))\n                    {\n                        spawnIniMissionIniSection.AddOrReplaceKey(\"LS640BkgdName\", lsFilename);\n                        spawnIniMissionIniSection.AddOrReplaceKey(\"LS800BkgdName\", lsFilename);\n                    }\n                }\n                if (string.IsNullOrEmpty(loadingScreenPalName))\n                {\n                    string palFilename = CustomMissionHelper.CustomMissionSupplementDefinition.FirstOrDefault(x => x.extension.Equals(\"pal\", StringComparison.InvariantCultureIgnoreCase)).filename;\n\n                    if (!string.IsNullOrEmpty(palFilename))\n                        spawnIniMissionIniSection.AddOrReplaceKey(\"LS800BkgdPal\", palFilename);\n                }\n\n                // append the new IniSection\n                spawnIni.AddSection(spawnIniMissionIniSection);\n                spawnIni.SetStringValue(\"Settings\", \"ReadMissionSection\", \"Yes\");\n            }\n        }\n\n        private void ToggleControls(bool enabled)\n        {\n            btnLaunch.AllowClick = enabled;\n            btnCancel.AllowClick = enabled;\n            lbCampaignList.Enabled = enabled;\n            trbDifficultySelector.Enabled = enabled;\n\n            if (btnReturn is not null)\n                btnReturn.AllowClick = enabled;\n\n            foreach (IUserSetting setting in userSettings)\n            {\n                if (setting is SettingCheckBoxBase cb)\n                    cb.AllowChecking = enabled;\n                else if (setting is SettingDropDownBase dd)\n                    dd.AllowDropDown = enabled;\n            }\n        }\n\n        private int GetComputerDifficulty() =>\n            Math.Abs(trbDifficultySelector.Value - 2);\n\n        private void GameProcessExited_Callback()\n        {\n            WindowManager.AddCallback(new Action(GameProcessExited), null);\n        }\n\n        protected virtual void GameProcessExited()\n        {\n            GameProcessLogic.GameProcessExited -= GameProcessExited_Callback;\n\n            CustomMissionHelper.DeleteSupplementalMissionFiles();\n\n            // Logger.Log(\"GameProcessExited: Updating Discord Presence.\");\n            discordHandler.UpdatePresence();\n\n            if (!ClientConfiguration.Instance.ReturnToMainMenuOnMissionLaunch)\n                ToggleControls(true);\n\n            // Handle ResetToDefaultOnGameExit\n            {\n                // Reset campaign checkboxes\n                foreach (CampaignCheckBox cb in CheckBoxes)\n                {\n                    if (cb.ResetToDefaultOnGameExit)\n                        cb.ResetToDefault();\n                }\n\n                // Reset user settings\n                foreach (IUserSetting setting in userSettings)\n                {\n                    if (!setting.ResetToDefaultOnGameExit)\n                        continue;\n\n                    if (setting is SettingCheckBoxBase cb)\n                        cb.Checked = cb.DefaultValue;\n                    else if (setting is SettingDropDownBase dd)\n                        dd.SelectedIndex = dd.DefaultValue;\n                }\n\n                SaveSettings();\n            }\n\n        }\n\n        private void ReadMissionList()\n        {\n            ParseBattleIni(\"INI/Battle.ini\");\n\n            if (AllMissions.Count == 0)\n                ParseBattleIni(\"INI/\" + ClientConfiguration.Instance.BattleFSFileName);\n\n            LoadCustomMissions();\n\n            LoadMissionsWithFilter(null, disableCustomMissions: true, disableOfficialMissions: false);\n        }\n\n        private void LoadCustomMissions()\n        {\n            string customMissionsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ClientConfiguration.Instance.CustomMissionPath);\n            if (!Directory.Exists(customMissionsDirectory))\n                return;\n\n            string[] mapFiles = Directory.GetFiles(customMissionsDirectory, \"*.map\");\n            if (mapFiles.Length == 0)\n                return;\n\n            foreach (string mapFilePath in mapFiles)\n            {\n                var mapFile = new IniFile(mapFilePath);\n\n                IniSection clientMissionDataSection = mapFile.GetSection(\"ClientMissionConfig\");\n\n                if (clientMissionDataSection is null)\n                    continue;\n\n                IniSection? gameMissionDataSection = mapFile.GetSection(\"GameMissionConfig\");\n\n                string filename = new FileInfo(mapFilePath).Name;\n                string scenario = SafePath.CombineFilePath(ClientConfiguration.Instance.CustomMissionPath, filename);\n                Mission mission = Mission.NewCustomMission(clientMissionDataSection, missionCodeName: filename, scenario, gameMissionDataSection);\n                AddMission(mission);\n            }\n        }\n\n        /// <summary>\n        /// Parses a Battle(E).ini file. Returns true if succesful (file found), otherwise false.\n        /// </summary>\n        /// <param name=\"path\">The path of the file, relative to the game directory.</param>\n        /// <returns>True if succesful, otherwise false.</returns>\n        private bool ParseBattleIni(string path)\n        {\n            Logger.Log(\"Attempting to parse \" + path + \" to populate mission list.\");\n\n            FileInfo battleIniFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path);\n            if (!battleIniFileInfo.Exists)\n            {\n                Logger.Log(\"File \" + path + \" not found. Ignoring.\");\n                return false;\n            }\n\n            if (selectedMissions.Count > 0)\n            {\n                throw new InvalidOperationException(\"Loading multiple Battle*.ini files is not supported anymore.\");\n            }\n\n            var battleIni = new IniFile(battleIniFileInfo.FullName);\n\n            List<string> battleKeys = battleIni.GetSectionKeys(\"Battles\");\n\n            if (battleKeys == null)\n                return false; // File exists but [Battles] doesn't\n\n            for (int i = 0; i < battleKeys.Count; i++)\n            {\n                string battleEntry = battleKeys[i];\n                string battleSection = battleIni.GetStringValue(\"Battles\", battleEntry, \"NOT FOUND\");\n\n                if (!battleIni.SectionExists(battleSection))\n                    continue;\n\n                var mission = new Mission(battleIni.GetSection(battleSection), missionCodeName: battleEntry);\n                AddMission(mission);\n            }\n\n            Logger.Log(\"Finished parsing \" + path + \".\");\n            return true;\n        }\n\n        /// <summary>\n        /// Load or re-load missons with selected tags.\n        /// </summary>\n        /// <param name=\"selectedTags\">Missions with at lease one of which tags to be shown. As an exception, null means show all missions.</param>\n        /// <param name=\"loadCustomMissions\">True means show official missions. False means show custom missions.</param>\n        public void LoadMissionsWithFilter(ISet<string> selectedTags, bool disableCustomMissions = true, bool disableOfficialMissions = false)\n        {\n            selectedMissions.Clear();\n\n            lbCampaignList.IsChangingSize = true;\n\n            lbCampaignList.Clear();\n            lbCampaignList.SelectedIndex = -1;\n\n            // The following two lines are handled by LbCampaignList_SelectedIndexChanged\n            // tbMissionDescription.Text = string.Empty;\n            // btnLaunch.AllowClick = false;\n\n            // Select missions with the filter\n            IEnumerable<Mission> missions = AllMissions;\n            if (disableCustomMissions && disableOfficialMissions)\n            {\n                // do nothing\n            }\n            else if (disableCustomMissions)\n            {\n                missions = missions.Where(mission => !mission.IsCustomMission);\n            }\n            else if (disableOfficialMissions)\n            {\n                missions = missions.Where(mission => mission.IsCustomMission);\n            }\n            else\n            {\n                // do nothing\n            }\n\n            if (selectedTags != null)\n                missions = missions.Where(mission => mission.Tags.Intersect(selectedTags).Any()).ToList();\n            selectedMissions = missions.ToList();\n\n            // Update lbCampaignList with selected missions\n            foreach (Mission mission in selectedMissions)\n            {\n                var item = new XNAListBoxItem();\n                item.Text = mission.GUIName;\n                if (!mission.Enabled)\n                {\n                    item.TextColor = UISettings.ActiveSettings.DisabledItemColor;\n                }\n                else if (string.IsNullOrEmpty(mission.Scenario))\n                {\n                    item.TextColor = AssetLoader.GetColorFromString(\n                        ClientConfiguration.Instance.ListBoxHeaderColor);\n                    item.IsHeader = true;\n                    item.Selectable = false;\n                }\n                else\n                {\n                    item.TextColor = lbCampaignList.DefaultItemColor;\n                }\n\n                if (!string.IsNullOrEmpty(mission.IconPath))\n                    item.Texture = AssetLoader.LoadTexture(mission.IconPath + \"icon.png\");\n\n                lbCampaignList.AddItem(item);\n            }\n\n            lbCampaignList.IsChangingSize = false;\n            lbCampaignList.TopIndex = 0;\n        }\n\n        /// <summary>\n        /// Saves settings to an INI file on the file system.\n        /// </summary>\n        private void SaveSettings()\n        {\n            SaveUserSettings();\n            SaveCampaignSettings();\n        }\n\n        private void SaveUserSettings()\n        {\n            userSettings.ForEach(c => c.Save());\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        private void SaveCampaignSettings()\n        {\n            if (!ClientConfiguration.Instance.SaveCampaignGameOptions)\n                return;\n\n            try\n            {\n                FileInfo settingsFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH);\n\n                settingsFileInfo.Delete();\n\n                var settingsIni = new IniFile(settingsFileInfo.FullName);\n\n                foreach (CampaignDropDown dd in DropDowns)\n                    settingsIni.SetStringValue(\"GameOptions\", dd.Name, dd.SelectedIndex.ToString());\n\n                foreach (CampaignCheckBox cb in CheckBoxes)\n                    settingsIni.SetStringValue(\"GameOptions\", cb.Name, cb.Checked.ToString());\n\n                settingsIni.WriteIniFile();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"Saving campaign settings failed! Reason: {ex}\");\n            }\n        }\n\n        /// <summary>\n        /// Loads settings from an INI file on the file system.\n        /// </summary>\n        private void LoadSettings()\n        {\n            LoadUserSettings();\n            LoadCampaignSettings();\n        }\n\n        private void LoadUserSettings() => userSettings.ForEach(c => c.Load());\n\n        private void LoadCampaignSettings()\n        {\n            if (!ClientConfiguration.Instance.SaveCampaignGameOptions)\n                return;\n\n            var settingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, SETTINGS_PATH));\n\n            foreach (CampaignDropDown dd in DropDowns)\n            {\n                dd.SelectedIndex = settingsIni.GetIntValue(\"GameOptions\", dd.Name, dd.SelectedIndex);\n\n                if (dd.SelectedIndex > -1 && dd.SelectedIndex < dd.Items.Count)\n                    dd.SelectedIndex = dd.SelectedIndex;\n            }\n\n            foreach (CampaignCheckBox cb in CheckBoxes)\n                cb.Checked = settingsIni.GetBooleanValue(\"GameOptions\", cb.Name, cb.Checked);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            base.Draw(gameTime);\n        }\n\n        private void UpdateMissionPreview(string missionPreviewFileName)\n        {\n            if (pnlMissionPreview == null)\n                return;\n\n            if (pnlMissionPreviewBackgroundTextureNeedsDispose)\n            {\n                Debug.Assert(pnlMissionPreview.BackgroundTexture != null, \"Expected background texture to dispose, but it was null.\");\n\n                pnlMissionPreview.BackgroundTexture.Dispose();\n                pnlMissionPreview.BackgroundTexture = null;\n                pnlMissionPreviewBackgroundTextureNeedsDispose = false;\n            }\n\n            string previewFilePath = null;\n            if (!string.IsNullOrEmpty(missionPreviewFileName))\n                previewFilePath = SafePath.CombineFilePath(missionPreviewFolder, missionPreviewFileName);\n\n            if (string.IsNullOrEmpty(missionPreviewFileName) || !File.Exists(previewFilePath))\n            {\n                pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture(\n                    AssetLoader.LoadTextureUncached(defaultMissionPreviewPath), pnlMissionPreview.Width, pnlMissionPreview.Height);\n            }\n            else\n            {\n                pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture(\n                    AssetLoader.LoadTextureUncached(previewFilePath), pnlMissionPreview.Width, pnlMissionPreview.Height);\n            }\n\n            pnlMissionPreviewBackgroundTextureNeedsDispose = true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Campaign/CampaignTagSelector.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nusing ClientCore;\n\nusing ClientGUI;\n\nusing DTAClient.Domain;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Campaign\n{\n    public class CampaignTagSelector : INItializableWindow\n    {\n        private const int DEFAULT_WIDTH = 576;\n        private const int DEFAULT_HEIGHT = 475;\n        private DiscordHandler discordHandler;\n\n        public CampaignTagSelector(WindowManager windowManager, DiscordHandler discordHandler)\n            : base(windowManager)\n        {\n            this.discordHandler = discordHandler;\n        }\n\n        public IReadOnlyDictionary<int, Mission> UniqueIDToMissions => CampaignSelector.UniqueIDToMissions;\n        public IReadOnlyCollection<Mission> AllMissions => CampaignSelector.AllMissions;\n\n        protected XNAClientButton btnCancel;\n        protected XNAClientButton btnShowAllMission;\n\n        public override void Initialize()\n        {\n            CampaignSelector = new CampaignSelector(WindowManager, discordHandler, this);\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, CampaignSelector);\n            CampaignSelector.Disable();\n            Name = nameof(CampaignTagSelector);\n\n            if (!ClientConfiguration.Instance.CampaignTagSelectorEnabled)\n                return;\n\n            ClientRectangle = new Rectangle(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);\n            BorderColor = UISettings.ActiveSettings.PanelBorderColor;\n\n            base.Initialize();\n\n            WindowManager.CenterControlOnScreen(this);\n\n            btnCancel = FindChild<XNAClientButton>(nameof(btnCancel));\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            btnShowAllMission = FindChild<XNAClientButton>(nameof(btnShowAllMission));\n            btnShowAllMission.LeftClick += (sender, e) =>\n            {\n                CampaignSelector.LoadMissionsWithFilter(null, disableCustomMissions: false, disableOfficialMissions: false);\n                NoFadeSwitch();\n            };\n\n            const string TagButtonsPrefix = \"ButtonTag_\";\n            var tagButtons = FindChildrenStartWith<XNAClientButton>(TagButtonsPrefix);\n            foreach (var tagButton in tagButtons)\n            {\n                if (tagButton.Enabled)\n                {\n                    string tagName = tagButton.Name.Substring(TagButtonsPrefix.Length);\n                    tagButton.LeftClick += (sender, e) =>\n                    {\n                        CampaignSelector.LoadMissionsWithFilter(new HashSet<string>() { tagName }, disableCustomMissions: false, disableOfficialMissions: false);\n                        NoFadeSwitch();\n                    };\n                }\n                else\n                {\n                    tagButton.AllowClick = false;\n                }\n            }\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        public void Open()\n        {\n            if (ClientConfiguration.Instance.CampaignTagSelectorEnabled)\n                Enable();\n            else\n                CampaignSelector.Enable();\n        }\n\n        public void NoFadeSwitch()\n        {\n            var dp = CampaignSelector.Parent as DarkeningPanel;\n            dp?.ToggleFade(false);\n\n            if (Visible)\n                CampaignSelector.Enable();\n            else\n                CampaignSelector.Disable();\n\n            dp?.ToggleFade(true);\n            dp = Parent as DarkeningPanel;\n            dp?.ToggleFade(false);\n\n            if (Visible)\n                Disable();\n            else\n                Enable();\n\n            dp?.ToggleFade(true);\n        }\n\n        private CampaignSelector CampaignSelector;\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Campaign/CheaterWindow.cs",
    "content": "﻿using System;\n\nusing ClientCore.Extensions;\n\nusing ClientGUI;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Campaign\n{\n    public class CheaterWindow : XNAWindow\n    {\n        public CheaterWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public event EventHandler YesClicked;\n\n        public override void Initialize()\n        {\n            Name = \"CheaterScreen\";\n            ClientRectangle = new Rectangle(0, 0, 334, 453);\n            BackgroundTexture = AssetLoader.LoadTexture(\"cheaterbg.png\");\n\n            var lblCheater = new XNALabel(WindowManager);\n            lblCheater.Name = nameof(lblCheater);\n            lblCheater.ClientRectangle = new Rectangle(0, 0, 0, 0);\n            lblCheater.FontIndex = 1;\n            lblCheater.Text = \"CHEATER!\".L10N(\"Client:Main:Cheater\");\n\n            var lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.ClientRectangle = new Rectangle(12, 40, 0, 0);\n            lblDescription.Text = (\"Modified game files have been detected. They could affect\\n\" +\n                \"the game experience.\\n\\n\" +\n                \"Do you really lack the skill for winning the mission without\\ncheating?\").L10N(\"Client:Main:CheaterText\");\n\n            var imagePanel = new XNAPanel(WindowManager);\n            imagePanel.Name = nameof(imagePanel);\n            imagePanel.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            imagePanel.ClientRectangle = new Rectangle(lblDescription.X,\n                lblDescription.Bottom + 12, Width - 24,\n                Height - (lblDescription.Bottom + 59));\n            imagePanel.BackgroundTexture = AssetLoader.LoadTextureUncached(\"cheater.png\");\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.ClientRectangle = new Rectangle(Width - 104,\n                Height - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            var btnYes = new XNAClientButton(WindowManager);\n            btnYes.Name = nameof(btnYes);\n            btnYes.ClientRectangle = new Rectangle(12, btnCancel.Y,\n                btnCancel.Width, btnCancel.Height);\n            btnYes.Text = \"Yes\".L10N(\"Client:Main:ButtonYes\");\n            btnYes.LeftClick += BtnYes_LeftClick;\n\n            AddChild(lblCheater);\n            AddChild(lblDescription);\n            AddChild(imagePanel);\n            AddChild(btnCancel);\n            AddChild(btnYes);\n\n            lblCheater.CenterOnParent();\n            lblCheater.ClientRectangle = new Rectangle(lblCheater.X, 12,\n                lblCheater.Width, lblCheater.Height);\n\n            base.Initialize();\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        private void BtnYes_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n            YesClicked?.Invoke(this, EventArgs.Empty);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/GameClass.cs",
    "content": "using ClientCore;\nusing ClientGUI;\nusing ClientGUI.IME;\nusing DTAClient.Domain;\nusing DTAClient.DXGUI.Generic;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Content;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Buffers.Binary;\nusing System.Diagnostics;\nusing System.IO;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Campaign;\nusing DTAClient.DXGUI.Multiplayer;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing DTAClient.Online;\nusing ClientGUI.Settings;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Rampastring.XNAUI.XNAControls;\nusing MainMenu = DTAClient.DXGUI.Generic.MainMenu;\nusing System.Threading.Tasks;\n\n#if WINFORMS\nusing System.Windows.Forms;\n#endif\n\nnamespace DTAClient.DXGUI\n{\n    /// <summary>\n    /// The main class for the game. Sets up asset search paths\n    /// and initializes components.\n    /// </summary>\n    public class GameClass : Game\n    {\n        public GameClass()\n        {\n            graphics = new GraphicsDeviceManager(this);\n            graphics.SynchronizeWithVerticalRetrace = false;\n#if !XNA\n            graphics.HardwareModeSwitch = false;\n\n            // Enable HiDef on a large monitor.\n            if (!ScreenResolution.HiDefLimitResolution.Fits(ScreenResolution.DesktopResolution))\n            {\n                // Enabling HiDef profile drops legacy GPUs not supporting DirectX 10.\n                // In practice, it's recommended to have a DirectX 11 capable GPU.\n                graphics.GraphicsProfile = GraphicsProfile.HiDef;\n            }\n#endif\n            content = new ContentManager(Services);\n        }\n\n        private static GraphicsDeviceManager graphics;\n        ContentManager content;\n\n        protected override void Initialize()\n        {\n            Logger.Log(\"Initializing GameClass.\");\n\n            string windowTitle = ClientConfiguration.Instance.WindowTitle;\n            Window.Title = string.IsNullOrEmpty(windowTitle) ?\n                string.Format(\"{0} Client\", MainClientConstants.GAME_NAME_SHORT) : windowTitle;\n\n            {\n                string developBuildTitle = \"Development Build\".L10N(\"Client:Main:DevelopmentBuildTitle\");\n\n#if DEVELOPMENT_BUILD\n                if (ClientConfiguration.Instance.ShowDevelopmentBuildWarnings)\n                    Window.Title += $\" ({developBuildTitle})\";\n#endif\n            }\n\n            base.Initialize();\n\n            AssetLoader.Initialize(GraphicsDevice, content);\n            AssetLoader.AssetSearchPaths.Add(UserINISettings.Instance.TranslationThemeFolderPath);\n            AssetLoader.AssetSearchPaths.Add(ProgramConstants.GetResourcePath());\n            AssetLoader.AssetSearchPaths.Add(UserINISettings.Instance.TranslationFolderPath);\n            AssetLoader.AssetSearchPaths.Add(ProgramConstants.GetBaseResourcePath());\n            AssetLoader.AssetSearchPaths.Add(ProgramConstants.GamePath);\n\n#if DX || (GL && WINFORMS)\n            // Try to create and load a texture to check for MonoGame compatibility\n#if DX\n            const string startupFailureFile = \".dxfail\";\n#elif GL && WINFORMS\n            const string startupFailureFile = \".oglfail\";\n#endif\n\n            try\n            {\n                Texture2D texture = new Texture2D(GraphicsDevice, 10, 10, false, SurfaceFormat.Color);\n                Color[] colorArray = new Color[10 * 10];\n                texture.SetData(colorArray);\n\n                _ = AssetLoader.LoadTextureUncached(\"checkBoxClear.png\");\n            }\n            catch (Exception ex)\n            {\n                // TODO Get English exception message\n                if (ex.Message.Contains(\"DeviceRemoved\"))\n                {\n                    Logger.Log($\"Creating texture on startup failed! Creating {startupFailureFile} file and re-launching client launcher.\");\n\n                    DirectoryInfo clientDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath);\n\n                    if (!clientDirectory.Exists)\n                        clientDirectory.Create();\n\n                    // Create startup failure file that the launcher can check for this error\n                    // and handle it by redirecting the user to another version instead\n\n                    File.WriteAllBytes(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 });\n\n                    string launcherExe = ClientConfiguration.Instance.LauncherExe;\n                    if (string.IsNullOrEmpty(launcherExe))\n                    {\n                        // LauncherExe is unspecified, just throw the exception forward\n                        // because we can't handle it\n\n                        Logger.Log(\"No LauncherExe= specified in ClientDefinitions.ini! \" +\n                            \"Forwarding exception to regular exception handler.\");\n\n                        throw;\n                    }\n                    else\n                    {\n                        Logger.Log(\"Starting \" + launcherExe + \" and exiting.\");\n\n                        Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe));\n                        Environment.Exit(1);\n                    }\n                }\n            }\n\n#endif\n            InitializeUISettings();\n\n            WindowManager wm = new(this, graphics);\n            wm.Initialize(content, ProgramConstants.GetBaseResourcePath());\n\n            IServiceProvider serviceProvider = null;\n            Task buildServiceProviderTask = Task.Run(() => { serviceProvider = BuildServiceProvider(wm); });\n\n            IMEHandler imeHandler = IMEHandler.Create(this);\n            wm.IMEHandler = imeHandler;\n\n            wm.ControlINIAttributeParsers.Add(new TranslationINIParser());\n\n            SetGraphicsMode(wm);\n\n#if WINFORMS\n            wm.SetIcon(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"clienticon.ico\"));\n            wm.SetControlBox(true);\n\n            // Enable resizable window for non-borderless windowed client, if integer scaling is enabled\n            if (!UserINISettings.Instance.BorderlessWindowedClient && UserINISettings.Instance.IntegerScaledClient)\n            {\n                wm.SetFormBorderStyle(FormBorderStyle.Sizable);\n                wm.SetMaximizeBox(true);\n\n                //// Automatically update render resolution when the window size changes\n                //// Disabled for now. It does not work as expected.\n                //// To fix this, we need to make every window and control to be able to handle window size changes.\n                //// This is not a trivial work and does not gain much benefit since the minimum render resolution and the maximum one are close.\n                //// Example: https://github.com/Rampastring/WorldAlteringEditor/blob/71d9bd0ed9b9843d5dc15de14005f86b18e5465c/src/TSMapEditor/UI/Controls/INItializableWindow.cs#L98\n\n                //ScreenResolution lastWindowSizeCaptured = new(wm.Game.Window.ClientBounds);\n\n                //wm.WindowSizeChangedByUser += (sender, e) =>\n                //{\n                //    ScreenResolution currentWindowSize = new(wm.Game.Window.ClientBounds);\n\n                //    if (currentWindowSize != lastWindowSizeCaptured)\n                //    {\n                //        Logger.Log($\"Window size changed from {lastWindowSizeCaptured} to {currentWindowSize}.\");\n                //        lastWindowSizeCaptured = currentWindowSize;\n                //        SetGraphicsMode(wm, currentWindowSize.Width, currentWindowSize.Height, centerOnScreen: false);\n                //    }\n                //};\n\n                wm.WindowSizeChangedByUser += (sender, e) =>\n                {\n                    imeHandler.SetIMETextInputRectangle(wm);\n                };\n            }\n#endif\n\n            wm.Cursor.Textures = new Texture2D[]\n            {\n                AssetLoader.LoadTexture(\"cursor.png\"),\n                AssetLoader.LoadTexture(\"waitCursor.png\")\n            };\n\n#if WINFORMS\n            FileInfo primaryNativeCursorPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), \"cursor.cur\");\n            FileInfo alternativeNativeCursorPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), \"cursor.cur\");\n\n            if (primaryNativeCursorPath.Exists)\n                wm.Cursor.LoadNativeCursor(primaryNativeCursorPath.FullName);\n            else if (alternativeNativeCursorPath.Exists)\n                wm.Cursor.LoadNativeCursor(alternativeNativeCursorPath.FullName);\n\n#endif\n            Components.Add(wm);\n\n            string playerName = UserINISettings.Instance.PlayerName.Value.Trim();\n\n            if (UserINISettings.Instance.AutoRemoveUnderscoresFromName)\n            {\n                while (playerName.EndsWith(\"_\"))\n                    playerName = playerName.Substring(0, playerName.Length - 1);\n            }\n\n            if (string.IsNullOrEmpty(playerName))\n            {\n                playerName = Environment.UserName;\n\n                playerName = playerName.Substring(playerName.IndexOf(\"\\\\\") + 1);\n            }\n\n            playerName = Renderer.GetSafeString(NameValidator.GetValidOfflineName(playerName), 0);\n\n            ProgramConstants.PLAYERNAME = playerName;\n            UserINISettings.Instance.PlayerName.Value = playerName;\n\n            buildServiceProviderTask.GetAwaiter().GetResult();\n\n            Logger.Log(\"Initializing loading screen.\");\n            LoadingScreen ls = serviceProvider.GetService<LoadingScreen>();\n            wm.AddAndInitializeControl(ls);\n            ls.ClientRectangle = new Rectangle((wm.RenderResolutionX - ls.Width) / 2,\n                (wm.RenderResolutionY - ls.Height) / 2, ls.Width, ls.Height);\n        }\n\n        private static Random GetRandom()\n        {\n            var rng = System.Security.Cryptography.RandomNumberGenerator.Create();\n            byte[] intBytes = new byte[sizeof(int)];\n            rng.GetBytes(intBytes);\n            int seed = BinaryPrimitives.ReadInt32LittleEndian(intBytes);\n            return new Random(seed);\n        }\n\n        private IServiceProvider BuildServiceProvider(WindowManager windowManager)\n        {\n            // Create host - this allows for things like DependencyInjection\n            IHost host = Host.CreateDefaultBuilder()\n                .ConfigureServices((_, services) =>\n                    {\n                        // services (or service-like)\n                        services\n                            .AddSingleton<ServiceProvider>()\n                            .AddSingleton(windowManager)\n                            .AddSingleton(GraphicsDevice)\n                            .AddSingleton<GameCollection>()\n                            .AddSingleton<CnCNetUserData>()\n                            .AddSingleton<CnCNetManager>()\n                            .AddSingleton<TunnelHandler>()\n                            .AddSingleton<DiscordHandler>()\n                            .AddSingleton<PrivateMessageHandler>()\n                            .AddSingleton<MapLoader>()\n                            .AddSingleton<Random>(GetRandom())\n                            .AddSingleton<DirectDrawWrapperManager>();\n\n                        // singleton xna controls - same instance on each request\n                        services\n                            .AddSingletonXnaControl<LoadingScreen>()\n                            .AddSingletonXnaControl<TopBar>()\n                            .AddSingletonXnaControl<OptionsWindow>()\n                            .AddSingletonXnaControl<PrivateMessagingWindow>()\n                            .AddSingletonXnaControl<PrivateMessagingPanel>()\n                            .AddSingletonXnaControl<LANLobby>()\n                            .AddSingletonXnaControl<CnCNetGameLobby>()\n                            .AddSingletonXnaControl<CnCNetGameLoadingLobby>()\n                            .AddSingletonXnaControl<CnCNetLobby>()\n                            .AddSingletonXnaControl<GameInProgressWindow>()\n                            .AddSingletonXnaControl<SkirmishLobby>()\n                            .AddSingletonXnaControl<MainMenu>()\n                            .AddSingletonXnaControl<MapPreviewBox>()\n                            .AddSingletonXnaControl<GameLaunchButton>()\n                            .AddSingletonXnaControl<PlayerExtraOptionsPanel>()\n                            .AddSingletonXnaControl<CampaignTagSelector>()\n                            .AddSingletonXnaControl<GameLoadingWindow>()\n                            .AddSingletonXnaControl<StatisticsWindow>()\n                            .AddSingletonXnaControl<UpdateQueryWindow>()\n                            .AddSingletonXnaControl<ManualUpdateQueryWindow>()\n                            .AddSingletonXnaControl<UpdateWindow>()\n                            .AddSingletonXnaControl<ExtrasWindow>();\n\n                        // transient xna controls - new instance on each request\n                        services\n                            .AddTransientXnaControl<XNAControl>()\n                            .AddTransientXnaControl<XNAButton>()\n                            .AddTransientXnaControl<XNAClientButton>()\n                            .AddTransientXnaControl<XNAClientCheckBox>()\n                            .AddTransientXnaControl<XNAClientDropDown>()\n                            .AddTransientXnaControl<XNALinkButton>()\n                            .AddTransientXnaControl<XNAExtraPanel>()\n                            .AddTransientXnaControl<XNACheckBox>()\n                            .AddTransientXnaControl<XNADropDown>()\n                            .AddTransientXnaControl<XNALabel>()\n                            .AddTransientXnaControl<XNALinkLabel>()\n                            .AddTransientXnaControl<XNAClientLinkLabel>()\n                            .AddTransientXnaControl<XNAListBox>()\n                            .AddTransientXnaControl<XNAMultiColumnListBox>()\n                            .AddTransientXnaControl<XNAPanel>()\n                            .AddTransientXnaControl<XNAProgressBar>()\n                            .AddTransientXnaControl<XNASuggestionTextBox>()\n                            .AddTransientXnaControl<XNATextBox>()\n                            .AddTransientXnaControl<XNATextBlock>()\n                            .AddTransientXnaControl<XNATrackbar>()\n                            .AddTransientXnaControl<XNAChatTextBox>()\n                            .AddTransientXnaControl<ChatListBox>()\n                            .AddTransientXnaControl<GameLobbyCheckBox>()\n                            .AddTransientXnaControl<GameLobbyDropDown>()\n                            .AddTransientXnaControl<CampaignCheckBox>()\n                            .AddTransientXnaControl<CampaignDropDown>()\n                            .AddTransientXnaControl<SettingCheckBox>()\n                            .AddTransientXnaControl<SettingDropDown>()\n                            .AddTransientXnaControl<FileSettingCheckBox>()\n                            .AddTransientXnaControl<FileSettingDropDown>();\n                    }\n                )\n                .Build();\n\n            return host.Services.GetService<IServiceProvider>();\n        }\n\n        private void InitializeUISettings()\n        {\n            UISettings settings = new UISettings();\n\n            settings.AltColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIColor);\n            settings.SubtleTextColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.UIHintTextColor);\n            settings.ButtonTextColor = settings.AltColor;\n            settings.ButtonHoverColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ButtonHoverColor);\n            settings.TextColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.UILabelColor);\n            //settings.WindowBorderColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.WindowBorderColor);\n            settings.PanelBorderColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.PanelBorderColor);\n            settings.BackgroundColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor);\n            settings.FocusColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ListBoxFocusColor);\n            settings.DisabledItemColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.DisabledButtonColor);\n\n            settings.DefaultAlphaRate = ClientConfiguration.Instance.DefaultAlphaRate;\n            settings.CheckBoxAlphaRate = ClientConfiguration.Instance.CheckBoxAlphaRate;\n            settings.IndicatorAlphaRate = ClientConfiguration.Instance.IndicatorAlphaRate;\n\n            settings.CheckBoxClearTexture = AssetLoader.LoadTexture(\"checkBoxClear.png\");\n            settings.CheckBoxCheckedTexture = AssetLoader.LoadTexture(\"checkBoxChecked.png\");\n            settings.CheckBoxDisabledClearTexture = AssetLoader.LoadTexture(\"checkBoxClearD.png\");\n            settings.CheckBoxDisabledCheckedTexture = AssetLoader.LoadTexture(\"checkBoxCheckedD.png\");\n\n            XNAPlayerSlotIndicator.LoadTextures();\n\n            UISettings.ActiveSettings = settings;\n        }\n\n        /// <summary>\n        /// Sets the client's graphics mode.\n        /// TODO move to some helper class?\n        /// </summary>\n        /// <param name=\"wm\">The window manager</param>\n        /// <param name=\"centerOnScreen\">Whether to center the client window on the screen</param>\n        public static void SetGraphicsMode(WindowManager wm, bool centerOnScreen = true)\n        {\n            int windowWidth = UserINISettings.Instance.ClientResolutionX;\n            int windowHeight = UserINISettings.Instance.ClientResolutionY;\n\n            SetGraphicsMode(wm, windowWidth, windowHeight, centerOnScreen);\n        }\n\n        /// <inheritdoc cref=\"SetGraphicsMode(WindowManager, bool)\"/>\n        /// <param name=\"windowWidth\">The viewport width</param>\n        /// <param name=\"windowHeight\">The viewport height</param>\n        public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool centerOnScreen = true)\n        {\n            bool borderlessWindowedClient = UserINISettings.Instance.BorderlessWindowedClient;\n            bool integerScale = UserINISettings.Instance.IntegerScaledClient;\n\n            SetGraphicsMode(wm, windowWidth, windowHeight, borderlessWindowedClient, integerScale, centerOnScreen);\n        }\n\n        /// <inheritdoc cref=\"SetGraphicsMode(WindowManager, int, int, bool)\"/>\n        /// <param name=\"borderlessWindowedClient\">Whether to use borderless windowed mode</param>\n        /// <param name=\"integerScale\">Whether to use integer scaling</param>\n        public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool borderlessWindowedClient, bool integerScale, bool centerOnScreen = true)\n        {\n            var clientConfiguration = ClientConfiguration.Instance;\n\n            (int desktopWidth, int desktopHeight) = ScreenResolution.SafeMaximumResolution;\n\n            if (desktopWidth >= windowWidth && desktopHeight >= windowHeight)\n            {\n                if (!wm.InitGraphicsMode(windowWidth, windowHeight, false))\n                    throw new GraphicsModeInitializationException(\"Setting graphics mode failed!\".L10N(\"Client:Main:SettingGraphicModeFailed\") + \" \" + windowWidth + \"x\" + windowHeight);\n            }\n            else\n            {\n                // fallback to the minimum supported resolution when the desktop is not sufficient to contain the client\n                // e.g., when users set a lower desktop resolution but the client resolution in the settings file remains high\n                if (!wm.InitGraphicsMode(1024, 600, false))\n                    throw new GraphicsModeInitializationException(\"Setting default graphics mode failed!\".L10N(\"Client:Main:SettingDefaultGraphicModeFailed\"));\n            }\n\n            int renderResolutionX = 0;\n            int renderResolutionY = 0;\n\n            if (!integerScale || windowWidth < clientConfiguration.MinimumRenderWidth || windowHeight < clientConfiguration.MinimumRenderHeight)\n            {\n                int initialXRes = Math.Max(windowWidth, clientConfiguration.MinimumRenderWidth);\n                initialXRes = Math.Min(initialXRes, clientConfiguration.MaximumRenderWidth);\n\n                int initialYRes = Math.Max(windowHeight, clientConfiguration.MinimumRenderHeight);\n                initialYRes = Math.Min(initialYRes, clientConfiguration.MaximumRenderHeight);\n\n                double xRatio = (windowWidth) / (double)initialXRes;\n                double yRatio = (windowHeight) / (double)initialYRes;\n\n                double ratio = xRatio > yRatio ? yRatio : xRatio;\n\n                // Special rule for 1360x768 and 1366x768                \n                if ((windowWidth == 1366 || windowWidth == 1360) && windowHeight == 768)\n                {\n                    // Most client interface has been designed for 1280x720 or 1280x800.\n                    // 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\n                    // This behavior is enforced even if IntegerScaledClient is turned off.\n                    renderResolutionX = windowWidth;\n                    renderResolutionY = windowHeight;\n                }\n\n                // Special rule: if 1280x720 is a valid render resolution, we allow 1.5x scaling for 1920x1080.\n                if (windowWidth == 1920 && windowHeight == 1080\n                    && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth\n                    && 720 >= clientConfiguration.MinimumRenderHeight && 720 <= clientConfiguration.MaximumRenderHeight)\n                {\n                    renderResolutionX = 1280;\n                    renderResolutionY = 720;\n                }\n\n                // Special rule: if 1280x800 is a valid render resolution, we allow 1.5x scaling for 1920x1200.\n                if (windowWidth == 1920 && windowHeight == 1200\n                    && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth\n                    && 800 >= clientConfiguration.MinimumRenderHeight && 800 <= clientConfiguration.MaximumRenderHeight)\n                {\n                    renderResolutionX = 1280;\n                    renderResolutionY = 800;\n                }\n\n                // Check whether we could integer-scale our client window\n                if (ratio > 1.0)\n                {\n                    for (int i = 2; i <= ScreenResolution.MAX_INT_SCALE; i++)\n                    {\n                        int sharpScaleRenderResX = windowWidth / i;\n                        int sharpScaleRenderResY = windowHeight / i;\n\n                        if (sharpScaleRenderResX >= clientConfiguration.MinimumRenderWidth &&\n                            sharpScaleRenderResX <= clientConfiguration.MaximumRenderWidth &&\n                            sharpScaleRenderResY >= clientConfiguration.MinimumRenderHeight &&\n                            sharpScaleRenderResY <= clientConfiguration.MaximumRenderHeight)\n                        {\n                            renderResolutionX = sharpScaleRenderResX;\n                            renderResolutionY = sharpScaleRenderResY;\n                            break;\n                        }\n                    }\n                }\n\n                // No special rules are triggered. Just zoom the client to the window size with minimal black bars.\n                if (renderResolutionX == 0 || renderResolutionY == 0)\n                {\n                    renderResolutionX = initialXRes;\n                    renderResolutionY = initialYRes;\n\n                    if (ratio == xRatio)\n                        renderResolutionY = (int)(windowHeight / ratio);\n                }\n            }\n            else\n            {\n                // Compute integer scale ratio using minimum render resolution\n                // Note: this means we prefer larger scale ratio than render resolution.\n                // This policy works best when maximum and minimum render resolution are close.\n                int xScale = windowWidth / clientConfiguration.MinimumRenderWidth;\n                int yScale = windowHeight / clientConfiguration.MinimumRenderHeight;\n                int scale = Math.Min(xScale, yScale);\n\n                // Compute render resolution\n                renderResolutionX = Math.Min(clientConfiguration.MaximumRenderWidth,\n                    clientConfiguration.MinimumRenderWidth + (windowWidth - clientConfiguration.MinimumRenderWidth * scale) / scale);\n                renderResolutionY = Math.Min(clientConfiguration.MaximumRenderHeight,\n                    clientConfiguration.MinimumRenderHeight + (windowHeight - clientConfiguration.MinimumRenderHeight * scale) / scale);\n            }\n\n            wm.SetBorderlessMode(borderlessWindowedClient);\n\n#if !XNA\n\n            if (borderlessWindowedClient)\n            {\n                // Note: on fullscreen mode, the client resolution must exactly match the desktop resolution. Otherwise buttons outside of client resolution are unclickable.\n                ScreenResolution clientResolution = (windowWidth, windowHeight);\n                if (ScreenResolution.DesktopResolution == clientResolution)\n                {\n                    Logger.Log($\"Entering fullscreen mode with resolution {ScreenResolution.DesktopResolution}.\");\n                    graphics.IsFullScreen = true;\n                    graphics.ApplyChanges();\n                }\n                else\n                {\n                    Logger.Log($\"Not entering fullscreen mode due to resolution mismatch. Desktop: {ScreenResolution.DesktopResolution}, Client: {clientResolution}.\");\n                }\n            }\n\n#endif\n            if (centerOnScreen)\n                wm.CenterOnScreen();\n\n            Logger.Log(\"Setting render resolution to \" + renderResolutionX + \"x\" + renderResolutionY + \". Integer scaling: \" + integerScale);\n            wm.IntegerScalingOnly = integerScale;\n            wm.SetRenderResolution(renderResolutionX, renderResolutionY);\n        }\n    }\n\n    /// <summary>\n    /// An exception that is thrown when initializing display / graphics mode fails.\n    /// </summary>\n    class GraphicsModeInitializationException : Exception\n    {\n        public GraphicsModeInitializationException(string message) : base(message)\n        {\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/DropDownDataWriteMode.cs",
    "content": "﻿namespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// An enum for controlling how the game lobbies'\n    /// drop-down controls' data should be written into the spawn INI.\n    /// </summary>\n    public enum DropDownDataWriteMode\n    {\n        /// <summary>\n        /// The 0-based selected index of the drop-down control will\n        /// be written into the INI.\n        /// </summary>\n        INDEX,\n\n        /// <summary>\n        /// If index 0 is selected, \"false\" will be written.\n        /// Otherwise the client will write \"true\".\n        /// </summary>\n        BOOLEAN,\n\n        /// <summary>\n        /// The dropdown value displayed in the UI will\n        /// be written into the INI.\n        /// </summary>\n        STRING,\n\n        /// <summary>\n        /// The dropdown value is filename of a mapcode INI file, which will be applied to the map. \n        /// Nothing is written to spawn INI.\n        /// </summary>\n        MAPCODE\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/ExtrasWindow.cs",
    "content": "﻿using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.Tools;\nusing System;\nusing System.Diagnostics;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    public class ExtrasWindow : XNAWindow\n    {\n        private StatisticsWindow statisticsWindow;\n\n        public ExtrasWindow(WindowManager windowManager, StatisticsWindow statisticsWindow) : base(windowManager)\n        {\n            this.statisticsWindow = statisticsWindow;\n        }\n\n        public override void Initialize()\n        {\n            Name = \"ExtrasWindow\";\n            ClientRectangle = new Rectangle(0, 0, 284, 190);\n            BackgroundTexture = AssetLoader.LoadTexture(\"extrasMenu.png\");\n\n            var btnExStatistics = new XNAClientButton(WindowManager);\n            btnExStatistics.Name = nameof(btnExStatistics);\n            btnExStatistics.ClientRectangle = new Rectangle(76, 17, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnExStatistics.Text = \"Statistics\".L10N(\"Client:Main:Statistics\");\n            btnExStatistics.LeftClick += BtnExStatistics_LeftClick;\n\n            var btnExMapEditor = new XNAClientButton(WindowManager);\n            btnExMapEditor.Name = nameof(btnExMapEditor);\n            btnExMapEditor.ClientRectangle = new Rectangle(76, 59, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnExMapEditor.Text = \"Map Editor\".L10N(\"Client:Main:MapEditor\");\n            btnExMapEditor.LeftClick += BtnExMapEditor_LeftClick;\n\n            var btnExCredits = new XNAClientButton(WindowManager);\n            btnExCredits.Name = nameof(btnExCredits);\n            btnExCredits.ClientRectangle = new Rectangle(76, 101, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnExCredits.Text = \"Credits\".L10N(\"Client:Main:Credits\");\n            btnExCredits.LeftClick += BtnExCredits_LeftClick;\n\n            var btnExCancel = new XNAClientButton(WindowManager);\n            btnExCancel.Name = nameof(btnExCancel);\n            btnExCancel.ClientRectangle = new Rectangle(76, 160, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnExCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnExCancel.LeftClick += BtnExCancel_LeftClick;\n\n            AddChild(btnExStatistics);\n            AddChild(btnExMapEditor);\n            AddChild(btnExCredits);\n            AddChild(btnExCancel);\n\n            base.Initialize();\n\n            CenterOnParent();\n        }\n\n        private void BtnExStatistics_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n            statisticsWindow.Enable();\n        }\n\n        private void BtnExMapEditor_LeftClick(object sender, EventArgs e)\n        {\n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n            using var mapEditorProcess = new Process();\n\n            if (osVersion != OSVersion.UNIX)\n                mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MapEditorExePath);\n            else\n                mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.UnixMapEditorExePath);\n\n            mapEditorProcess.StartInfo.UseShellExecute = false;\n\n            mapEditorProcess.Start();\n\n            Disable();\n        }\n\n        private void BtnExCredits_LeftClick(object sender, EventArgs e)\n        {\n            ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.CreditsURL);\n        }\n\n        private void BtnExCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/GameInProgressWindow.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.Tools;\nusing System;\nusing ClientCore;\nusing Rampastring.XNAUI;\nusing ClientGUI;\nusing System.IO;\nusing ClientCore.Extensions;\nusing ClientCore.Enums;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\nusing System.Linq;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.DXGUI\n{\n    /// <summary>\n    /// Displays a dialog in the client when a game is in progress.\n    /// Also enables power-saving (lowers FPS) while a game is in progress,\n    /// and performs various operations on game start and exit.\n    /// </summary>\n    public class GameInProgressWindow : XNAPanel\n    {\n        private const double POWER_SAVING_FPS = 5.0;\n\n        public GameInProgressWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        private bool initialized = false;\n        private bool nativeCursorUsed = false;\n\n        private List<string> debugSnapshotDirectories;\n        private DateTime debugLogLastWriteTime;\n        private bool deletingLogFilesFailed = false;\n\n        public override void Initialize()\n        {\n            if (initialized)\n                throw new InvalidOperationException(\"GameInProgressWindow cannot be initialized twice!\");\n\n            initialized = true;\n\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            DrawBorders = false;\n            ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY);\n\n            XNAWindow window = new XNAWindow(WindowManager);\n\n            window.Name = \"GameInProgressWindow\";\n            window.BackgroundTexture = AssetLoader.LoadTexture(\"gameinprogresswindowbg.png\");\n            window.ClientRectangle = new Rectangle(0, 0, 200, 100);\n\n            XNALabel explanation = new XNALabel(WindowManager);\n            explanation.Text = \"A game is in progress.\".L10N(\"Client:Main:GameInProgress\");\n\n            AddChild(window);\n\n            window.AddChild(explanation);\n\n            base.Initialize();\n\n            GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted;\n            GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited;\n\n            explanation.CenterOnParent();\n\n            window.CenterOnParent();\n\n            Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / UserINISettings.Instance.ClientFPS);\n\n            Visible = false;\n            Enabled = false;\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares)\n            {\n                try\n                {\n                    FileInfo debugLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, \"debug\", \"debug.log\");\n\n                    if (debugLogFileInfo.Exists)\n                        debugLogLastWriteTime = debugLogFileInfo.LastWriteTime;\n                }\n                catch { }\n            }\n\n        }\n\n        private void SharedUILogic_GameProcessStarted()\n        {\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares)\n            {\n                debugSnapshotDirectories = GetAllDebugSnapshotDirectories();\n            }\n            else\n            {\n                try\n                {\n                    SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"EXCEPT.TXT\");\n\n                    for (int i = 0; i < 8; i++)\n                        SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"SYNC\" + i + \".TXT\");\n\n                    deletingLogFilesFailed = false;\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Exception when deleting error log files! Message: \" + ex.ToString());\n                    deletingLogFilesFailed = true;\n                }\n            }\n\n            Visible = true;\n            Enabled = true;\n            WindowManager.Cursor.Visible = false;\n            nativeCursorUsed = Game.IsMouseVisible;\n            Game.IsMouseVisible = false;\n            ProgramConstants.IsInGame = true;\n            Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / POWER_SAVING_FPS);\n#if WINFORMS\n\n            if (UserINISettings.Instance.MinimizeWindowsOnGameStart)\n                WindowManager.MinimizeWindow();\n#endif\n        }\n\n        private void SharedUILogic_GameProcessExited()\n        {\n            WindowManager.AddCallback(new Action(HandleGameProcessExited), null);\n        }\n\n        private void HandleGameProcessExited()\n        {\n            Visible = false;\n            Enabled = false;\n            if (nativeCursorUsed)\n                Game.IsMouseVisible = true;\n            else\n                WindowManager.Cursor.Visible = true;\n            ProgramConstants.IsInGame = false;\n            Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / UserINISettings.Instance.ClientFPS);\n\n#if WINFORMS\n            if (UserINISettings.Instance.MinimizeWindowsOnGameStart)\n                WindowManager.MaximizeWindow();\n\n#endif\n            UserINISettings.Instance.ReloadSettings();\n\n            if (UserINISettings.Instance.BorderlessWindowedClient)\n            {\n                // Hack: Re-set graphics mode\n                // Windows resizes our window if we're in fullscreen mode and\n                // the in-game resolution is lower than the user's desktop resolution.\n                // After the game exits, Windows doesn't properly re-size our window\n                // back to cover the entire screen, which causes graphics to get\n                // stretched and also messes up input handling since the window manager\n                // still thinks it's using the original resolution.\n                // Re-setting the graphics mode fixes it.\n                GameClass.SetGraphicsMode(WindowManager);\n            }\n\n            DateTime dtn = DateTime.Now;\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares)\n            {\n                Task.Run(ProcessScreenshots);\n\n                // TODO: Ares debug log handling should be addressed in Ares DLL itself.\n                // For now the following are handled here:\n                // 1. Make a copy of syringe.log in debug snapshot directory on both crash and desync.\n                // 2. Move SYNCX.txt from game directory to debug snapshot directory on desync.\n                // 3. Make a debug snapshot directory & copy debug.log to it on desync even if full crash dump wasn't created.\n                // 4. Handle the empty snapshot directories created on a crash if debug logging was disabled.\n\n                string snapshotDirectory = GetNewestDebugSnapshotDirectory();\n                bool snapshotCreated = snapshotDirectory != null;\n\n                snapshotDirectory = snapshotDirectory ?? SafePath.CombineDirectoryPath(ProgramConstants.GamePath, \"debug\", FormattableString.Invariant($\"snapshot-{dtn.ToString(\"yyyyMMdd-HHmmss\")}\"));\n\n                bool debugLogModified = false;\n                FileInfo debugLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, \"debug\", \"debug.log\");\n                DateTime lastWriteTime = new DateTime();\n\n                if (debugLogFileInfo.Exists)\n                    lastWriteTime = debugLogFileInfo.LastAccessTime;\n\n                if (!lastWriteTime.Equals(debugLogLastWriteTime))\n                {\n                    debugLogModified = true;\n                    debugLogLastWriteTime = lastWriteTime;\n                }\n\n                if (CopySyncErrorLogs(snapshotDirectory, null) || snapshotCreated)\n                {\n                    FileInfo snapShotDebugLogFileInfo = SafePath.GetFile(snapshotDirectory, \"debug.log\");\n\n                    if (debugLogFileInfo.Exists && !snapShotDebugLogFileInfo.Exists && debugLogModified)\n                        File.Copy(debugLogFileInfo.FullName, snapShotDebugLogFileInfo.FullName);\n\n                    CopyErrorLog(snapshotDirectory, \"syringe.log\", null);\n                }\n            }\n            else\n            {\n                if (deletingLogFilesFailed)\n                    return;\n\n                CopyErrorLog(SafePath.CombineDirectoryPath(ProgramConstants.ClientUserFilesPath, \"GameCrashLogs\"), \"EXCEPT.TXT\", dtn);\n                CopySyncErrorLogs(SafePath.CombineDirectoryPath(ProgramConstants.ClientUserFilesPath, \"SyncErrorLogs\"), dtn);\n            }\n        }\n\n        /// <summary>\n        /// Attempts to copy a general error log from game directory to another directory.\n        /// </summary>\n        /// <param name=\"directory\">Directory to copy error log to.</param>\n        /// <param name=\"filename\">Filename of the error log.</param>\n        /// <param name=\"dateTime\">Time to to apply as a timestamp to filename. Set to null to not apply a timestamp.</param>\n        /// <returns>True if error log was copied, false otherwise.</returns>\n        private bool CopyErrorLog(string directory, string filename, DateTime? dateTime)\n        {\n            bool copied = false;\n\n            try\n            {\n                FileInfo errorLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, filename);\n\n                if (errorLogFileInfo.Exists)\n                {\n                    DirectoryInfo errorLogDirectoryInfo = SafePath.GetDirectory(directory);\n\n                    if (!errorLogDirectoryInfo.Exists)\n                        errorLogDirectoryInfo.Create();\n\n                    Logger.Log(\"The game crashed! Copying \" + filename + \" file.\");\n\n                    string timeStamp = dateTime.HasValue ? dateTime.Value.ToString(\"_yyyy_MM_dd_HH_mm\") : \"\";\n\n                    string filenameCopy = Path.GetFileNameWithoutExtension(filename) +\n                        timeStamp + Path.GetExtension(filename);\n\n                    File.Copy(errorLogFileInfo.FullName, SafePath.CombineFilePath(directory, filenameCopy));\n                    copied = true;\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"An error occured while checking for \" + filename + \" file. Message: \" + ex.ToString());\n            }\n            return copied;\n        }\n\n        /// <summary>\n        /// Attempts to copy sync error logs from game directory to another directory.\n        /// </summary>\n        /// <param name=\"directory\">Directory to copy sync error logs to.</param>\n        /// <param name=\"dateTime\">Time to to apply as a timestamp to filename. Set to null to not apply a timestamp.</param>\n        /// <returns>True if any sync logs were copied, false otherwise.</returns>\n        private bool CopySyncErrorLogs(string directory, DateTime? dateTime)\n        {\n            bool copied = false;\n\n            try\n            {\n                for (int i = 0; i < 8; i++)\n                {\n                    string filename = \"SYNC\" + i + \".TXT\";\n                    FileInfo syncErrorLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, filename);\n\n                    if (syncErrorLogFileInfo.Exists)\n                    {\n                        DirectoryInfo syncErrorLogDirectoryInfo = SafePath.GetDirectory(directory);\n\n                        if (!syncErrorLogDirectoryInfo.Exists)\n                            syncErrorLogDirectoryInfo.Create();\n\n                        Logger.Log(\"There was a sync error! Copying file \" + filename);\n\n                        string timeStamp = dateTime.HasValue ? dateTime.Value.ToString(\"_yyyy_MM_dd_HH_mm\") : \"\";\n\n                        string filenameCopy = Path.GetFileNameWithoutExtension(filename) +\n                            timeStamp + Path.GetExtension(filename);\n\n                        File.Copy(syncErrorLogFileInfo.FullName, SafePath.CombineFilePath(directory, filenameCopy));\n                        copied = true;\n                        syncErrorLogFileInfo.Delete();\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"An error occured while checking for SYNCX.TXT files. Message: \" + ex.ToString());\n            }\n            return copied;\n        }\n\n        /// <summary>\n        /// Returns the first debug snapshot directory found in Ares debug log directory that was created after last game launch and isn't empty.\n        /// Additionally any empty snapshot directories encountered are deleted.\n        /// </summary>\n        /// <returns>Full path of the debug snapshot directory. If one isn't found, null is returned.</returns>\n        private string GetNewestDebugSnapshotDirectory()\n        {\n            string snapshotDirectory = null;\n\n            if (debugSnapshotDirectories != null)\n            {\n                var newDirectories = GetAllDebugSnapshotDirectories().Except(debugSnapshotDirectories);\n\n                foreach (string directory in newDirectories)\n                {\n                    if (Directory.EnumerateFileSystemEntries(directory).Any())\n                        snapshotDirectory = directory;\n                    else\n                    {\n                        try\n                        {\n                            Directory.Delete(directory);\n                        }\n                        catch { }\n                    }\n                }\n            }\n\n            return snapshotDirectory;\n        }\n\n        /// <summary>\n        /// Returns list of all debug snapshot directories in Ares debug logs directory.\n        /// </summary>\n        /// <returns>List of all debug snapshot directories in Ares debug logs directory. Empty list if none are found or an error was encountered.</returns>\n        private List<string> GetAllDebugSnapshotDirectories()\n        {\n            var directories = new List<string>();\n\n            try\n            {\n                directories.AddRange(Directory.GetDirectories(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, \"debug\"), \"snapshot-*\"));\n            }\n            catch { }\n\n            return directories;\n        }\n\n        /// <summary>\n        /// Converts BMP screenshots to PNG and copies them from game directory to Screenshots sub-directory.\n        /// </summary>\n        private void ProcessScreenshots()\n        {\n            IEnumerable<FileInfo> files = SafePath.GetDirectory(ProgramConstants.GamePath).EnumerateFiles(\"SCRN*.bmp\");\n            DirectoryInfo screenshotsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, \"Screenshots\");\n\n            if (!screenshotsDirectory.Exists)\n            {\n                try\n                {\n                    screenshotsDirectory.Create();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"ProcessScreenshots: An error occured trying to create Screenshots directory. Message: \" + ex.ToString());\n                    return;\n                }\n            }\n\n            foreach (FileInfo file in files)\n            {\n                try\n                {\n                    using FileStream stream = file.OpenRead();\n                    using var image = Image.Load(stream);\n                    FileInfo newFile = SafePath.GetFile(screenshotsDirectory.FullName, FormattableString.Invariant($\"{Path.GetFileNameWithoutExtension(file.FullName)}.png\"));\n                    using FileStream newFileStream = newFile.OpenWrite();\n\n                    image.SaveAsPng(newFileStream);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"ProcessScreenshots: Error occured when trying to save \" + Path.GetFileNameWithoutExtension(file.FullName) + \".png. Message: \" + ex.ToString());\n                    continue;\n                }\n\n                Logger.Log(\"ProcessScreenshots: \" + Path.GetFileNameWithoutExtension(file.FullName) + \".png has been saved to Screenshots directory.\");\n                file.Delete();\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/GameLoadingWindow.cs",
    "content": "using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Diagnostics;\nusing DTAClient.DXGUI.Campaign;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// A window for loading saved singleplayer games.\n    /// </summary>\n    public class GameLoadingWindow : XNAWindow\n    {\n        private const string SAVED_GAMES_DIRECTORY = \"Saved Games\";\n\n        public GameLoadingWindow(WindowManager windowManager, DiscordHandler discordHandler, CampaignTagSelector campaignTagSelector) : base(windowManager)\n        {\n            this.discordHandler = discordHandler;\n            this.campaignTagSelector = campaignTagSelector;\n        }\n\n        private DiscordHandler discordHandler;\n        private CampaignTagSelector campaignTagSelector;\n\n        private XNAMultiColumnListBox lbSaveGameList;\n        private XNAClientButton btnLaunch;\n        private XNAClientButton btnDelete;\n        private XNAClientButton btnCancel;\n\n        private List<SavedGame> savedGames = new List<SavedGame>();\n\n        public override void Initialize()\n        {\n            Name = \"GameLoadingWindow\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"loadmissionbg.png\");\n\n            ClientRectangle = new Rectangle(0, 0, 600, 380);\n            CenterOnParent();\n\n            lbSaveGameList = new XNAMultiColumnListBox(WindowManager);\n            lbSaveGameList.Name = nameof(lbSaveGameList);\n            lbSaveGameList.ClientRectangle = new Rectangle(13, 13, 574, 317);\n            lbSaveGameList.AddColumn(\"SAVED GAME NAME\".L10N(\"Client:Main:SavedGameNameColumnHeader\"), 400);\n            lbSaveGameList.AddColumn(\"DATE / TIME\".L10N(\"Client:Main:SavedGameDateTimeColumnHeader\"), 174);\n            lbSaveGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbSaveGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbSaveGameList.SelectedIndexChanged += ListBox_SelectedIndexChanged;\n            lbSaveGameList.AllowKeyboardInput = true;\n\n            btnLaunch = new XNAClientButton(WindowManager);\n            btnLaunch.Name = nameof(btnLaunch);\n            btnLaunch.ClientRectangle = new Rectangle(125, 345, 110, 23);\n            btnLaunch.Text = \"Load\".L10N(\"Client:Main:ButtonLoad\");\n            btnLaunch.AllowClick = false;\n            btnLaunch.LeftClick += BtnLaunch_LeftClick;\n\n            btnDelete = new XNAClientButton(WindowManager);\n            btnDelete.Name = nameof(btnDelete);\n            btnDelete.ClientRectangle = new Rectangle(btnLaunch.Right + 10, btnLaunch.Y, 110, 23);\n            btnDelete.Text = \"Delete\".L10N(\"Client:Main:ButtonDelete\");\n            btnDelete.AllowClick = false;\n            btnDelete.LeftClick += BtnDelete_LeftClick;\n\n            btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.ClientRectangle = new Rectangle(btnDelete.Right + 10, btnLaunch.Y, 110, 23);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(lbSaveGameList);\n            AddChild(btnLaunch);\n            AddChild(btnDelete);\n            AddChild(btnCancel);\n\n            base.Initialize();\n\n            ListSaves();\n        }\n\n        private void ListBox_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (lbSaveGameList.SelectedIndex == -1)\n            {\n                btnLaunch.AllowClick = false;\n                btnDelete.AllowClick = false;\n            }\n            else\n            {\n                btnLaunch.AllowClick = true;\n                btnDelete.AllowClick = true;\n            }\n        }\n\n        public void Open()\n        {\n            Enable();\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        private void BtnLaunch_LeftClick(object sender, EventArgs e)\n        {\n            SavedGame sg = savedGames[lbSaveGameList.SelectedIndex];\n            Logger.Log(\"Loading saved game \" + sg.FileName);\n\n            Mission mission = campaignTagSelector.UniqueIDToMissions.GetValueOrDefault(sg.CustomMissionID, null);\n\n            CustomMissionHelper.DeleteSupplementalMissionFiles();\n\n            if (mission != null)\n                CustomMissionHelper.CopySupplementalMissionFiles(mission);\n\n            FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS);\n\n            if (spawnerSettingsFile.Exists)\n                spawnerSettingsFile.Delete();\n\n            IniFile spawnIni = new()\n            {\n                Comment = \"Generated by CnCNet Client\"\n            };\n\n            IniSection spawnIniSettings = new(\"Settings\");\n            spawnIniSettings.AddKey(\"Scenario\", \"spawnmap.ini\");\n            spawnIniSettings.AddKey(\"SaveGameName\", sg.FileName);\n            spawnIniSettings.AddKey(\"LoadSaveGame\", \"Yes\");\n            spawnIniSettings.AddKey(\"SidebarHack\", ClientConfiguration.Instance.SidebarHack.ToString());\n            spawnIniSettings.AddKey(\"CustomLoadScreen\", LoadingScreenController.GetLoadScreenName(\"g\"));\n            spawnIniSettings.AddKey(\"Firestorm\", \"No\");\n            spawnIniSettings.AddKey(\"GameSpeed\", UserINISettings.Instance.GameSpeed.ToString());\n\n            spawnIni.AddSection(spawnIniSettings);\n\n            if (mission != null)\n            {\n                spawnIniSettings.AddKey(\"CustomMissionID\", sg.CustomMissionID.ToString());\n                CampaignSelector.WriteMissionSectionToSpawnIni(spawnIni, mission);\n            }\n\n            spawnIni.WriteIniFile(spawnerSettingsFile.FullName);\n\n            FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, \"spawnmap.ini\");\n\n            if (spawnMapIniFile.Exists)\n                spawnMapIniFile.Delete();\n\n            using (var spawnMapStreamWriter = new StreamWriter(spawnMapIniFile.FullName))\n            {\n                spawnMapStreamWriter.WriteLine(\"[Map]\");\n                spawnMapStreamWriter.WriteLine(\"Size=0,0,50,50\");\n                spawnMapStreamWriter.WriteLine(\"LocalSize=0,0,50,50\");\n                spawnMapStreamWriter.WriteLine();\n            }\n\n            discordHandler.UpdatePresence(sg.GUIName, true);\n\n            Disable();\n            GameProcessLogic.GameProcessExited += GameProcessExited_Callback;\n\n            GameProcessLogic.StartGameProcess(WindowManager);\n        }\n\n        private void BtnDelete_LeftClick(object sender, EventArgs e)\n        {\n            SavedGame sg = savedGames[lbSaveGameList.SelectedIndex];\n            var msgBox = new XNAMessageBox(WindowManager, \"Delete Confirmation\".L10N(\"Client:Main:DeleteConfirmationTitle\"),\n                string.Format((\"The following saved game will be deleted permanently:\\n\\n\" +\n                    \"Filename: {0}\\n\" +\n                    \"Saved game name: {1}\\n\" +\n                    \"Date and time: {2}\\n\\n\" +\n                    \"Are you sure you want to proceed?\").L10N(\"Client:Main:DeleteConfirmationText\"),\n                    sg.FileName, Renderer.GetSafeString(sg.GUIName, lbSaveGameList.FontIndex), sg.LastModified.ToString()),\n                XNAMessageBoxButtons.YesNo);\n            msgBox.Show();\n            msgBox.YesClickedAction = DeleteMsgBox_YesClicked;\n        }\n\n        private void DeleteMsgBox_YesClicked(XNAMessageBox obj)\n        {\n            SavedGame sg = savedGames[lbSaveGameList.SelectedIndex];\n\n            Logger.Log(\"Deleting saved game \" + sg.FileName);\n            SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, sg.FileName);\n            ListSaves();\n        }\n\n        private void GameProcessExited_Callback()\n        {\n            WindowManager.AddCallback(new Action(GameProcessExited), null);\n        }\n\n        protected virtual void GameProcessExited()\n        {\n            GameProcessLogic.GameProcessExited -= GameProcessExited_Callback;\n\n            CustomMissionHelper.DeleteSupplementalMissionFiles();\n\n            discordHandler.UpdatePresence();\n        }\n\n        public void ListSaves()\n        {\n            savedGames.Clear();\n            lbSaveGameList.ClearItems();\n            lbSaveGameList.SelectedIndex = -1;\n\n            DirectoryInfo savedGamesDirectoryInfo = SafePath.GetDirectory(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY);\n\n            if (!savedGamesDirectoryInfo.Exists)\n            {\n                Logger.Log(\"Saved Games directory not found!\");\n                return;\n            }\n\n            IEnumerable<FileInfo> files = savedGamesDirectoryInfo.EnumerateFiles(\"*.SAV\", SearchOption.TopDirectoryOnly);\n\n            foreach (FileInfo file in files)\n            {\n                // Note: ParseSaveGame modifies savedGames\n                ParseSaveGame(file.FullName);\n            }\n\n            savedGames = savedGames.OrderBy(sg => sg.LastModified.Ticks).ToList();\n            savedGames.Reverse();\n\n            foreach (SavedGame sg in savedGames)\n            {\n                string[] item = new string[] {\n                    Renderer.GetSafeString(sg.GUIName, lbSaveGameList.FontIndex),\n                    sg.LastModified.ToString() };\n                lbSaveGameList.AddItem(item, true);\n            }\n        }\n\n        private void ParseSaveGame(string fileName)\n        {\n            string shortName = Path.GetFileName(fileName);\n\n            SavedGame sg = new SavedGame(shortName);\n            if (sg.ParseInfo())\n                savedGames.Add(sg);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/GameSessionCheckBox.cs",
    "content": "﻿using System;\n\nusing ClientGUI;\n\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Generic;\n\npublic enum CheckBoxMapScoringMode\n{\n    /// <summary>\n    /// The value of the check box makes no difference for scoring maps.\n    /// </summary>\n    Irrelevant = 0,\n\n    /// <summary>\n    /// The check box prevents map scoring when it's checked.\n    /// </summary>\n    DenyWhenChecked = 1,\n\n    /// <summary>\n    /// The check box prevents map scoring when it's unchecked.\n    /// </summary>\n    DenyWhenUnchecked = 2\n}\n\n/// <summary>\n/// A game option check box for the game lobby or campaign.\n/// </summary>\n// TODO split the logic between descendants better and clean up\npublic class GameSessionCheckBox : XNAClientCheckBox, IGameSessionSetting\n{\n    private const int DEFAULT_SORT_ORDER = 0;\n\n    public GameSessionCheckBox(WindowManager windowManager) : base (windowManager) { }\n\n    public bool AllowChanges { get; set; } = true;\n\n    public bool AffectsSpawnIni => !string.IsNullOrWhiteSpace(spawnIniOption);\n    public bool AffectsMapCode => !string.IsNullOrWhiteSpace(customIniPath);\n\n    public bool AllowScoring\n        => !((mapScoringMode == CheckBoxMapScoringMode.DenyWhenChecked && Checked)\n             || (mapScoringMode == CheckBoxMapScoringMode.DenyWhenUnchecked && !Checked));\n\n    private CheckBoxMapScoringMode mapScoringMode = CheckBoxMapScoringMode.Irrelevant;\n\n    private string spawnIniOption;\n\n    private string customIniPath;\n\n    protected bool reversed;\n\n    private string enabledSpawnIniValue = \"True\";\n    private string disabledSpawnIniValue = \"False\";\n\n    private bool DefaultChecked { get; set; }\n\n    /// <summary>\n    /// Whether this checkbox should be included in the GAME broadcast.\n    /// </summary>\n    public bool BroadcastToLobby { get; private set; }\n\n    /// <summary>\n    /// Whether the icon/text should be shown in the game list.\n    /// </summary>\n    public bool ShowInGameList { get; private set; }\n\n    /// <summary>\n    /// Whether the icon should be shown on the right side of the game list.\n    /// Only applies if ShowInGameList is true.\n    /// </summary>\n    public bool ShowInGameListOnRight { get; private set; }\n\n    /// <summary>\n    /// Whether the icon/text should be shown in the game information panel.\n    /// </summary>\n    public bool ShowInGameInformationPanel { get; private set; }\n\n    /// <summary>\n    /// Whether to show only the icon (without text) in the game information panel.\n    /// Only applies if ShowInGameInformationPanel is true.\n    /// </summary>\n    public bool ShowInGameInformationPanelAsIconOnly { get; private set; }\n\n    /// <summary>\n    /// Whether the icon should be shown in the game lobby control itself.\n    /// </summary>\n    public bool ShowIconInGameLobby { get; private set; }\n\n    /// <summary>\n    /// Whether this setting should be filterable and shown in the filters panel.\n    /// </summary>\n    public bool ShowInFilters { get; private set; }\n\n    /// <summary>\n    /// The texture name for the icon when setting is enabled.\n    /// </summary>\n    public string EnabledIcon { get; private set; }\n\n    /// <summary>\n    /// The texture name for the icon when setting is disabled.\n    /// </summary>\n    public string DisabledIcon { get; private set; }\n\n    /// <summary>\n    /// Sort order for displaying icons in the GameInformationPanel and GameListBox.\n    /// Lower values appear first.\n    /// </summary>\n    public int SortOrder { get; private set; } = DEFAULT_SORT_ORDER;\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        switch (key)\n        {\n            case \"SpawnIniOption\":\n                spawnIniOption = value;\n                return;\n            case \"EnabledSpawnIniValue\":\n                enabledSpawnIniValue = value;\n                return;\n            case \"DisabledSpawnIniValue\":\n                disabledSpawnIniValue = value;\n                return;\n            case \"CustomIniPath\":\n                customIniPath = value;\n                return;\n            case \"Reversed\":\n                reversed = Conversions.BooleanFromString(value, false);\n                return;\n            case \"Checked\":\n                bool checkedValue = Conversions.BooleanFromString(value, false);\n                DefaultChecked = Checked = checkedValue;\n                return;\n            case \"MapScoringMode\":\n                mapScoringMode = (CheckBoxMapScoringMode)Enum.Parse(typeof(CheckBoxMapScoringMode), value);\n                return;\n            case \"BroadcastToLobby\":\n                BroadcastToLobby = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameList\":\n                ShowInGameList = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameListOnRight\":\n                ShowInGameListOnRight = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameInformationPanel\":\n                ShowInGameInformationPanel = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameInformationPanelAsIconOnly\":\n                ShowInGameInformationPanelAsIconOnly = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowIconInGameLobby\":\n                ShowIconInGameLobby = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInFilters\":\n                ShowInFilters = Conversions.BooleanFromString(value, false);\n                return;\n            case \"EnabledIcon\":\n                EnabledIcon = value;\n                return;\n            case \"DisabledIcon\":\n                DisabledIcon = value;\n                return;\n            case \"SortOrder\":\n                SortOrder = int.Parse(value);\n                return;\n        }\n\n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n\n    public int Value\n    {\n        get => Checked ? 1 : 0;  // 0 = unchecked/off, 1 = checked/on\n        set => Checked = value != 0;  // 0 = unchecked/off, 1 = checked/on\n    }\n\n    public void ApplySpawnIniCode(IniFile spawnIni)\n    {\n        if (!AffectsSpawnIni)\n            return;\n\n        string value = disabledSpawnIniValue;\n        if (Checked != reversed)\n        {\n            value = enabledSpawnIniValue;\n        }\n\n        spawnIni.SetStringValue(\"Settings\", spawnIniOption, value);\n    }\n        \n    public void ApplyMapCode(IniFile mapIni, GameMode gameMode)\n    {\n        if (!AffectsMapCode || Checked == reversed)\n            return;\n\n        MapCodeHelper.ApplyMapCode(mapIni, customIniPath, gameMode);\n    }\n\n    public override void OnLeftClick(InputEventArgs inputEventArgs)\n    {\n        // FIXME there's a discrepancy with how base XNAUI handles this\n        // it doesn't set handled if changing the setting is not allowed\n        inputEventArgs.Handled = true;\n            \n        if (!AllowChanges)\n            return;\n\n        base.OnLeftClick(inputEventArgs);\n    }\n\n    public void ResetToDefault()\n    {\n        if (!AllowChanges)\n            throw new InvalidOperationException(\"Cannot reset to default when changes are not allowed.\");\n\n        Checked = DefaultChecked;\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/GameSessionDropDown.cs",
    "content": "﻿using System;\n\nusing ClientCore.Extensions;\nusing ClientCore.I18N;\n\nusing ClientGUI;\n\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Generic;\n\n/// <summary>\n/// A game option drop-down for the game lobby or campaign.\n/// </summary>\n// TODO split the logic between descendants better and clean up\npublic class GameSessionDropDown : XNAClientDropDown, IGameSessionSetting\n{\n\n    private const int DEFAULT_SORT_ORDER = 0;\n\n    public GameSessionDropDown(WindowManager windowManager) : base(windowManager) { }\n\n    public string OptionName { get; private set; }\n    public bool AffectsSpawnIni => dataWriteMode != DropDownDataWriteMode.MAPCODE;\n    public bool AffectsMapCode => dataWriteMode == DropDownDataWriteMode.MAPCODE;\n    public bool AllowScoring => true;  // TODO\n\n    private DropDownDataWriteMode dataWriteMode = DropDownDataWriteMode.BOOLEAN;\n\n    private string spawnIniOption = string.Empty;\n\n    private int defaultIndex;\n\n    /// <summary>\n    /// Whether this dropdown should be included in the GAME broadcast.\n    /// </summary>\n    public bool BroadcastToLobby { get; private set; }\n\n    /// <summary>\n    /// Whether the icon/text should be shown in the game list.\n    /// </summary>\n    public bool ShowInGameList { get; private set; }\n\n    /// <summary>\n    /// Whether the icon should be shown on the right side of the game list.\n    /// Only applies if ShowInGameList is true.\n    /// </summary>\n    public bool ShowInGameListOnRight { get; private set; }\n\n    /// <summary>\n    /// Whether the icon/text should be shown in the game information panel.\n    /// </summary>\n    public bool ShowInGameInformationPanel { get; private set; }\n\n    /// <summary>\n    /// Whether to show only the icon (without text) in the game information panel.\n    /// Only applies if ShowInGameInformationPanel is true.\n    /// </summary>\n    public bool ShowInGameInformationPanelAsIconOnly { get; private set; }\n\n    /// <summary>\n    /// Whether the icon should be shown in the game lobby control itself.\n    /// </summary>\n    public bool ShowIconInGameLobby { get; private set; }\n\n    /// <summary>\n    /// Whether this setting should be filterable and shown in the filters panel.\n    /// </summary>\n    public bool ShowInFilters { get; private set; }\n\n    /// <summary>\n    /// Sort order for displaying icons in the GameInformationPanel and GameListBox.\n    /// Lower values appear first.\n    /// </summary>\n    public int SortOrder { get; private set; } = DEFAULT_SORT_ORDER;\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        // shorthand for localization function\n        static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true)\n            => Translation.Instance.LookUp(control, attributeName, defaultValue, notify);\n\n        switch (key)\n        {\n            case \"Items\":\n                string[] items = value.SplitWithCleanup();\n                string[] itemLabels = iniFile.GetStringListValue(Name, \"ItemLabels\", \"\");\n                string[] iconNames = iniFile.GetStringListValue(Name, \"Icons\", \"\");\n                for (int i = 0; i < items.Length; i++)\n                {\n                    bool hasLabel = itemLabels.Length > i && !string.IsNullOrEmpty(itemLabels[i]);\n                    string iconName = iconNames.Length > i ? iconNames[i] : null;\n                    XNADropDownItem item = new()\n                    {\n                        Text = Localize(this, $\"Item{i}\",\n                            hasLabel ? itemLabels[i] : items[i]),\n                        Tag = items[i],\n                        Texture = !string.IsNullOrEmpty(iconName) ? AssetLoader.LoadTexture(iconName) : null,\n                    };\n                    AddItem(item);\n                }\n                return;\n            case \"DataWriteMode\":\n                if (value.ToUpper() == \"INDEX\")\n                    dataWriteMode = DropDownDataWriteMode.INDEX;\n                else if (value.ToUpper() == \"BOOLEAN\")\n                    dataWriteMode = DropDownDataWriteMode.BOOLEAN;\n                else if (value.ToUpper() == \"MAPCODE\")\n                    dataWriteMode = DropDownDataWriteMode.MAPCODE;\n                else\n                    dataWriteMode = DropDownDataWriteMode.STRING;\n                return;\n            case \"SpawnIniOption\":\n                spawnIniOption = value;\n                return;\n            case \"DefaultIndex\":\n                SelectedIndex = int.Parse(value);\n                defaultIndex = SelectedIndex;\n                return;\n            case \"OptionName\":\n                OptionName = Localize(this, \"OptionName\", value);\n                return;\n            case \"BroadcastToLobby\":\n                BroadcastToLobby = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameList\":\n                ShowInGameList = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameListOnRight\":\n                ShowInGameListOnRight = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameInformationPanel\":\n                ShowInGameInformationPanel = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInGameInformationPanelAsIconOnly\":\n                ShowInGameInformationPanelAsIconOnly = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowIconInGameLobby\":\n                ShowIconInGameLobby = Conversions.BooleanFromString(value, false);\n                return;\n            case \"ShowInFilters\":\n                ShowInFilters = Conversions.BooleanFromString(value, false);\n                return;\n            case \"SortOrder\":\n                SortOrder = int.Parse(value);\n                return;\n        }\n\n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n\n    public int Value\n    {\n        get => SelectedIndex;\n        set => SelectedIndex = value;\n    }\n\n    public void ApplySpawnIniCode(IniFile spawnIni)\n    {\n        if (!AffectsSpawnIni || SelectedIndex < 0 || SelectedIndex >= Items.Count)\n            return;\n\n        if (String.IsNullOrEmpty(spawnIniOption))\n        {\n            Logger.Log(\"GameLobbyDropDown.WriteSpawnIniCode: \" + Name + \" has no associated spawn INI option!\");\n            return;\n        }\n\n        switch (dataWriteMode)\n        {\n            case DropDownDataWriteMode.BOOLEAN:\n                spawnIni.SetBooleanValue(\"Settings\", spawnIniOption, SelectedIndex > 0);\n                break;\n            case DropDownDataWriteMode.INDEX:\n                spawnIni.SetIntValue(\"Settings\", spawnIniOption, SelectedIndex);\n                break;\n            default:\n            case DropDownDataWriteMode.STRING:\n                spawnIni.SetStringValue(\"Settings\", spawnIniOption, Items[SelectedIndex].Tag.ToString());\n                break;\n        }\n    }\n\n    public void ApplyMapCode(IniFile mapIni, GameMode gameMode)\n    {\n        if (!AffectsMapCode || SelectedIndex < 0 || SelectedIndex >= Items.Count) return;\n\n        string customIniPath;\n        customIniPath = Items[SelectedIndex].Tag.ToString();\n\n        MapCodeHelper.ApplyMapCode(mapIni, customIniPath, gameMode);\n    }\n\n    public override void OnLeftClick(InputEventArgs inputEventArgs)\n    {\n        // FIXME there's a discrepancy with how base XNAUI handles this\n        // it doesn't set handled if changing the setting is not allowed\n        inputEventArgs.Handled = true;\n            \n        if (!AllowDropDown)\n            return;\n\n        base.OnLeftClick(inputEventArgs);\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/LoadingScreen.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\nusing ClientUpdater;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Online;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System.Diagnostics;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    public class LoadingScreen : XNAWindow\n    {\n        public LoadingScreen(\n            CnCNetManager cncnetManager,\n            WindowManager windowManager,\n            IServiceProvider serviceProvider,\n            MapLoader mapLoader,\n            Random random\n        ) : base(windowManager)\n        {\n            this.cncnetManager = cncnetManager;\n            this.serviceProvider = serviceProvider;\n            this.mapLoader = mapLoader;\n            this.random = random;\n        }\n\n        private MapLoader mapLoader;\n\n        private Random random;\n\n        private bool visibleSpriteCursor;\n\n        private Task updaterInitTask;\n        private Task mapLoadTask;\n\n        private readonly CnCNetManager cncnetManager;\n        private readonly IServiceProvider serviceProvider;\n\n        private List<string> randomTextures;\n\n        public override void Initialize()\n        {\n            ClientRectangle = new Rectangle(0, 0, 800, 600);\n            Name = \"LoadingScreen\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"loadingscreen.png\");\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            bool initUpdater = !ClientConfiguration.Instance.ModMode;\n\n            if (initUpdater)\n            {\n                updaterInitTask = new Task(InitUpdater);\n                updaterInitTask.Start();\n            }\n\n            mapLoader.Initialize();\n            mapLoadTask = mapLoader.LoadMapsAsync();\n\n            if (Cursor.Visible)\n            {\n                Cursor.Visible = false;\n                visibleSpriteCursor = true;\n            }\n        }\n\n        protected override void GetINIAttributes(IniFile iniFile)\n        {\n            base.GetINIAttributes(iniFile);\n\n            randomTextures = iniFile.GetStringListValue(Name, \"RandomBackgroundTextures\", string.Empty).ToList();\n\n            if (randomTextures.Count == 0)\n                return;\n\n            BackgroundTexture = AssetLoader.LoadTexture(randomTextures[random.Next(randomTextures.Count)]);\n        }\n\n        private void InitUpdater()\n        {\n            Logger.Log(\"Updater: Updater initialization task started.\");\n\n            Updater.OnLocalFileVersionsChecked += LogGameClientVersion;\n            Updater.CheckLocalFileVersions();\n\n            Logger.Log(\"Updater: Updater initialization task completed.\");\n        }\n\n        private void LogGameClientVersion()\n        {\n            Logger.Log($\"Game Client Version: {ClientConfiguration.Instance.LocalGame} {Updater.GameVersion}\");\n            Updater.OnLocalFileVersionsChecked -= LogGameClientVersion;\n        }\n\n        private void Finish()\n        {\n            Logger.Log(\"LoadingScreen: Finish waiting for updater and map loading tasks. Proceeding to main menu.\");\n\n            ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ?\n                \"N/A\" : Updater.GameVersion;\n\n            MainMenu mainMenu = serviceProvider.GetService<MainMenu>();\n\n            WindowManager.AddAndInitializeControl(mainMenu);\n            mainMenu.PostInit();\n\n            if (UserINISettings.Instance.AutomaticCnCNetLogin &&\n                NameValidator.IsNameValid(ProgramConstants.PLAYERNAME, out _) == NameValidationError.None)\n            {\n                cncnetManager.Connect();\n            }\n\n            if (!UserINISettings.Instance.PrivacyPolicyAccepted)\n            {\n                WindowManager.AddAndInitializeControl(new PrivacyNotification(WindowManager));\n            }\n\n            WindowManager.RemoveControl(this);\n\n            Cursor.Visible = visibleSpriteCursor;\n        }\n\n\n        private TimeSpan Update_LastLogTime = TimeSpan.Zero;\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            bool updaterDone = updaterInitTask == null || updaterInitTask.Status == TaskStatus.RanToCompletion;\n            bool mapLoadDone = mapLoadTask.Status == TaskStatus.RanToCompletion;\n\n            if (updaterDone && mapLoadDone)\n            {\n                Finish();\n                return;\n            }\n\n            var timeSinceLastLog = gameTime.TotalGameTime.Subtract(Update_LastLogTime);\n            if (timeSinceLastLog > TimeSpan.FromSeconds(5))\n            {\n                Update_LastLogTime = gameTime.TotalGameTime;\n\n                string logMessage;\n                if (!updaterDone && !mapLoadDone)\n                    logMessage = \"LoadingScreen: Waiting for updater initialization and loading maps...\";\n                else if (!updaterDone)\n                    logMessage = \"LoadingScreen: Waiting for updater initialization...\";\n                else if (!mapLoadDone)\n                    logMessage = \"LoadingScreen: Waiting for loading maps...\";\n                else\n                    throw new Exception(\"Assert failed. No pending tasks. This should not happen.\");\n\n                Debug.WriteLine(logMessage);\n                Logger.Log(logMessage);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/MainMenu.cs",
    "content": "using ClientCore;\nusing ClientCore.Enums;\nusing ClientCore.I18N;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing DTAClient.Online;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Input;\nusing Microsoft.Xna.Framework.Media;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing ClientUpdater;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.DXGUI.Campaign;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// The main menu of the client.\n    /// </summary>\n    class MainMenu : XNAWindow, ISwitchable\n    {\n        private const float MEDIA_PLAYER_VOLUME_FADE_STEP = 0.01f;\n        private const float MEDIA_PLAYER_VOLUME_EXIT_FADE_STEP = 0.025f;\n        private const double UPDATE_RE_CHECK_THRESHOLD = 30.0;\n\n        /// <summary>\n        /// Creates a new instance of the main menu.\n        /// </summary>\n        public MainMenu(\n            WindowManager windowManager,\n            SkirmishLobby skirmishLobby,\n            LANLobby lanLobby,\n            TopBar topBar,\n            OptionsWindow optionsWindow,\n            CnCNetLobby cncnetLobby,\n            CnCNetManager connectionManager,\n            DiscordHandler discordHandler,\n            CnCNetGameLoadingLobby cnCNetGameLoadingLobby,\n            CnCNetGameLobby cnCNetGameLobby,\n            PrivateMessagingPanel privateMessagingPanel,\n            PrivateMessagingWindow privateMessagingWindow,\n            GameInProgressWindow gameInProgressWindow,\n            MapLoader mapLoader,\n            CampaignTagSelector campaignTagSelector,\n            GameLoadingWindow gameLoadingWindow,\n            StatisticsWindow statisticsWindow,\n            UpdateQueryWindow updateQueryWindow,\n            ManualUpdateQueryWindow manualUpdateQueryWindow,\n            UpdateWindow updateWindow,\n            ExtrasWindow extrasWindow,\n            DirectDrawWrapperManager directDrawWrapperManager\n        ) : base(windowManager)\n        {\n            this.lanLobby = lanLobby;\n            this.topBar = topBar;\n            this.connectionManager = connectionManager;\n            this.optionsWindow = optionsWindow;\n            this.cncnetLobby = cncnetLobby;\n            this.discordHandler = discordHandler;\n            this.skirmishLobby = skirmishLobby;\n            this.cnCNetGameLoadingLobby = cnCNetGameLoadingLobby;\n            this.cnCNetGameLobby = cnCNetGameLobby;\n            this.privateMessagingPanel = privateMessagingPanel;\n            this.privateMessagingWindow = privateMessagingWindow;\n            this.gameInProgressWindow = gameInProgressWindow;\n            this.mapLoader = mapLoader;\n            this.campaignTagSelector = campaignTagSelector;\n            this.gameLoadingWindow = gameLoadingWindow;\n            this.statisticsWindow = statisticsWindow;\n            this.updateQueryWindow = updateQueryWindow;\n            this.manualUpdateQueryWindow = manualUpdateQueryWindow;\n            this.updateWindow = updateWindow;\n            this.extrasWindow = extrasWindow;\n            this.directDrawWrapperManager = directDrawWrapperManager;\n\n            this.cncnetLobby.UpdateCheck += CncnetLobby_UpdateCheck;\n            isMediaPlayerAvailable = IsMediaPlayerAvailable();\n        }\n\n        private XNALabel lblCnCNetPlayerCount;\n        private XNALinkLabel lblUpdateStatus;\n        private XNALinkLabel lblVersion;\n\n        private CnCNetLobby cncnetLobby;\n\n        private SkirmishLobby skirmishLobby;\n\n        private LANLobby lanLobby;\n\n        private CnCNetManager connectionManager;\n\n        private OptionsWindow optionsWindow;\n\n        private DiscordHandler discordHandler;\n\n        private TopBar topBar;\n        private readonly CnCNetGameLoadingLobby cnCNetGameLoadingLobby;\n        private readonly CnCNetGameLobby cnCNetGameLobby;\n        private readonly PrivateMessagingPanel privateMessagingPanel;\n        private readonly PrivateMessagingWindow privateMessagingWindow;\n        private readonly GameInProgressWindow gameInProgressWindow;\n        private readonly MapLoader mapLoader;\n        private readonly CampaignTagSelector campaignTagSelector;\n        private readonly GameLoadingWindow gameLoadingWindow;\n        private readonly StatisticsWindow statisticsWindow;\n        private readonly UpdateQueryWindow updateQueryWindow;\n        private readonly ManualUpdateQueryWindow manualUpdateQueryWindow;\n        private readonly UpdateWindow updateWindow;\n        private readonly ExtrasWindow extrasWindow;\n        private readonly DirectDrawWrapperManager directDrawWrapperManager;\n\n        private XNAMessageBox firstRunMessageBox;\n\n        private bool _updateInProgress;\n        private bool UpdateInProgress\n        {\n            get { return _updateInProgress; }\n            set\n            {\n                _updateInProgress = value;\n                topBar.SetSwitchButtonsClickable(!_updateInProgress);\n                topBar.SetOptionsButtonClickable(!_updateInProgress);\n                SetButtonHotkeys(!_updateInProgress);\n            }\n        }\n\n        private bool customComponentDialogQueued = false;\n\n        private DateTime lastUpdateCheckTime;\n\n        private Song themeSong;\n\n        private static readonly object locker = new object();\n\n        private bool isMusicFading = false;\n\n        private readonly bool isMediaPlayerAvailable;\n\n        private CancellationTokenSource cncnetPlayerCountCancellationSource;\n\n        // Main Menu Buttons\n        private XNAClientButton btnNewCampaign;\n        private XNAClientButton btnLoadGame;\n        private XNAClientButton btnSkirmish;\n        private XNAClientButton btnCnCNet;\n        private XNAClientButton btnLan;\n        private XNAClientButton btnOptions;\n        private XNAClientButton btnMapEditor;\n        private XNAClientButton btnStatistics;\n        private XNAClientButton btnCredits;\n        private XNAClientButton btnExtras;\n\n        /// <summary>\n        /// Initializes the main menu's controls.\n        /// </summary>\n        public override void Initialize()\n        {\n            topBar.SetSecondarySwitch(cncnetLobby);\n            GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited;\n\n            Name = nameof(MainMenu);\n            BackgroundTexture = AssetLoader.LoadTexture(\"MainMenu/mainmenubg.png\");\n            ClientRectangle = new Rectangle(0, 0, BackgroundTexture.Width, BackgroundTexture.Height);\n\n            WindowManager.CenterControlOnScreen(this);\n\n            btnNewCampaign = new XNAClientButton(WindowManager);\n            btnNewCampaign.Name = nameof(btnNewCampaign);\n            btnNewCampaign.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/campaign.png\");\n            btnNewCampaign.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/campaign_c.png\");\n            btnNewCampaign.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnNewCampaign.LeftClick += BtnNewCampaign_LeftClick;\n\n            btnLoadGame = new XNAClientButton(WindowManager);\n            btnLoadGame.Name = nameof(btnLoadGame);\n            btnLoadGame.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/loadmission.png\");\n            btnLoadGame.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/loadmission_c.png\");\n            btnLoadGame.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnLoadGame.LeftClick += BtnLoadGame_LeftClick;\n\n            btnSkirmish = new XNAClientButton(WindowManager);\n            btnSkirmish.Name = nameof(btnSkirmish);\n            btnSkirmish.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/skirmish.png\");\n            btnSkirmish.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/skirmish_c.png\");\n            btnSkirmish.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnSkirmish.LeftClick += BtnSkirmish_LeftClick;\n\n            btnCnCNet = new XNAClientButton(WindowManager);\n            btnCnCNet.Name = nameof(btnCnCNet);\n            btnCnCNet.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/cncnet.png\");\n            btnCnCNet.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/cncnet_c.png\");\n            btnCnCNet.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnCnCNet.LeftClick += BtnCnCNet_LeftClick;\n\n            btnLan = new XNAClientButton(WindowManager);\n            btnLan.Name = nameof(btnLan);\n            btnLan.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/lan.png\");\n            btnLan.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/lan_c.png\");\n            btnLan.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnLan.LeftClick += BtnLan_LeftClick;\n\n            btnOptions = new XNAClientButton(WindowManager);\n            btnOptions.Name = nameof(btnOptions);\n            btnOptions.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/options.png\");\n            btnOptions.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/options_c.png\");\n            btnOptions.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnOptions.LeftClick += BtnOptions_LeftClick;\n\n            btnMapEditor = new XNAClientButton(WindowManager);\n            btnMapEditor.Name = nameof(btnMapEditor);\n            btnMapEditor.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/mapeditor.png\");\n            btnMapEditor.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/mapeditor_c.png\");\n            btnMapEditor.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnMapEditor.LeftClick += BtnMapEditor_LeftClick;\n\n            btnStatistics = new XNAClientButton(WindowManager);\n            btnStatistics.Name = nameof(btnStatistics);\n            btnStatistics.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/statistics.png\");\n            btnStatistics.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/statistics_c.png\");\n            btnStatistics.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnStatistics.LeftClick += BtnStatistics_LeftClick;\n\n            btnCredits = new XNAClientButton(WindowManager);\n            btnCredits.Name = nameof(btnCredits);\n            btnCredits.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/credits.png\");\n            btnCredits.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/credits_c.png\");\n            btnCredits.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnCredits.LeftClick += BtnCredits_LeftClick;\n\n            btnExtras = new XNAClientButton(WindowManager);\n            btnExtras.Name = nameof(btnExtras);\n            btnExtras.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/extras.png\");\n            btnExtras.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/extras_c.png\");\n            btnExtras.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnExtras.LeftClick += BtnExtras_LeftClick;\n\n            var btnExit = new XNAClientButton(WindowManager);\n            btnExit.Name = nameof(btnExit);\n            btnExit.IdleTexture = AssetLoader.LoadTexture(\"MainMenu/exitgame.png\");\n            btnExit.HoverTexture = AssetLoader.LoadTexture(\"MainMenu/exitgame_c.png\");\n            btnExit.HoverSoundEffect = new EnhancedSoundEffect(\"MainMenu/button.wav\");\n            btnExit.LeftClick += BtnExit_LeftClick;\n\n            XNALabel lblCnCNetStatus = new XNALabel(WindowManager);\n            lblCnCNetStatus.Name = nameof(lblCnCNetStatus);\n            lblCnCNetStatus.Text = \"DTA players on CnCNet:\".L10N(\"Client:Main:CnCNetOnlinePlayersCountText\");\n            lblCnCNetStatus.ClientRectangle = new Rectangle(12, 9, 0, 0);\n\n            lblCnCNetPlayerCount = new XNALabel(WindowManager);\n            lblCnCNetPlayerCount.Name = nameof(lblCnCNetPlayerCount);\n            lblCnCNetPlayerCount.Text = \"-\";\n\n            lblVersion = new XNALinkLabel(WindowManager);\n            lblVersion.Name = nameof(lblVersion);\n            lblVersion.LeftClick += LblVersion_LeftClick;\n\n            lblUpdateStatus = new XNALinkLabel(WindowManager);\n            lblUpdateStatus.Name = nameof(lblUpdateStatus);\n            lblUpdateStatus.LeftClick += LblUpdateStatus_LeftClick;\n            lblUpdateStatus.ClientRectangle = new Rectangle(0, 0, UIDesignConstants.BUTTON_WIDTH_160, 20);\n\n            AddChild(btnNewCampaign);\n            AddChild(btnLoadGame);\n            AddChild(btnSkirmish);\n            AddChild(btnCnCNet);\n            AddChild(btnLan);\n            AddChild(btnOptions);\n            AddChild(btnMapEditor);\n            AddChild(btnStatistics);\n            AddChild(btnCredits);\n            AddChild(btnExtras);\n            AddChild(btnExit);\n            AddChild(lblCnCNetStatus);\n            AddChild(lblCnCNetPlayerCount);\n\n            if (!ClientConfiguration.Instance.ModMode)\n            {\n                // ModMode disables version tracking and the updater if it's enabled\n\n                AddChild(lblVersion);\n                AddChild(lblUpdateStatus);\n\n                Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated;\n                Updater.OnCustomComponentsOutdated += Updater_OnCustomComponentsOutdated;\n            }\n\n            base.Initialize(); // Read control attributes from INI\n\n            lblVersion.Text = Updater.GameVersion;\n\n            updateQueryWindow.UpdateDeclined += UpdateQueryWindow_UpdateDeclined;\n            updateQueryWindow.UpdateAccepted += UpdateQueryWindow_UpdateAccepted;\n            manualUpdateQueryWindow.Closed += ManualUpdateQueryWindow_Closed;\n\n            updateWindow.UpdateCompleted += UpdateWindow_UpdateCompleted;\n            updateWindow.UpdateCancelled += UpdateWindow_UpdateCancelled;\n            updateWindow.UpdateFailed += UpdateWindow_UpdateFailed;\n\n            ClientRectangle = new Rectangle((WindowManager.RenderResolutionX - Width) / 2,\n                (WindowManager.RenderResolutionY - Height) / 2,\n                Width, Height);\n\n            CnCNetPlayerCountTask.CnCNetGameCountUpdated += CnCNetInfoController_CnCNetGameCountUpdated;\n            cncnetPlayerCountCancellationSource = new CancellationTokenSource();\n            CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource);\n\n            WindowManager.GameClosing += WindowManager_GameClosing;\n\n            skirmishLobby.Exited += SkirmishLobby_Exited;\n            lanLobby.Exited += LanLobby_Exited;\n            optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged;\n\n            optionsWindow.OnForceUpdate += (s, e) => ForceUpdate();\n\n            GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted;\n            GameProcessLogic.GameProcessStarting += SharedUILogic_GameProcessStarting;\n\n            UserINISettings.Instance.SettingsSaved += SettingsSaved;\n\n            Updater.Restart += Updater_Restart;\n\n            SetButtonHotkeys(true);\n        }\n\n        private void SetButtonHotkeys(bool enableHotkeys)\n        {\n            if (!Initialized)\n                return;\n\n            if (enableHotkeys)\n            {\n                btnNewCampaign.HotKey = Keys.C;\n                btnLoadGame.HotKey = Keys.L;\n                btnSkirmish.HotKey = Keys.S;\n                btnCnCNet.HotKey = Keys.M;\n                btnLan.HotKey = Keys.N;\n                btnOptions.HotKey = Keys.O;\n                btnMapEditor.HotKey = Keys.E;\n                btnStatistics.HotKey = Keys.T;\n                btnCredits.HotKey = Keys.R;\n                btnExtras.HotKey = Keys.X;\n            }\n            else\n            {\n                btnNewCampaign.HotKey = Keys.None;\n                btnLoadGame.HotKey = Keys.None;\n                btnSkirmish.HotKey = Keys.None;\n                btnCnCNet.HotKey = Keys.None;\n                btnLan.HotKey = Keys.None;\n                btnOptions.HotKey = Keys.None;\n                btnMapEditor.HotKey = Keys.None;\n                btnStatistics.HotKey = Keys.None;\n                btnCredits.HotKey = Keys.None;\n                btnExtras.HotKey = Keys.None;\n            }\n        }\n\n        private void OptionsWindow_EnabledChanged(object sender, EventArgs e)\n        {\n            if (!optionsWindow.Enabled)\n            {\n                if (customComponentDialogQueued)\n                    Updater_OnCustomComponentsOutdated();\n            }\n        }\n\n        /// <summary>\n        /// Refreshes settings. Called when the game process is starting.\n        /// </summary>\n        private void SharedUILogic_GameProcessStarting()\n        {\n            UserINISettings.Instance.ReloadSettings();\n\n            try\n            {\n                optionsWindow.RefreshSettings();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Refreshing settings failed! Exception message: \" + ex.ToString());\n                // We don't want to show the dialog when starting a game\n                //XNAMessageBox.Show(WindowManager, \"Saving settings failed\",\n                //    \"Saving settings failed! Error message: \" + ex.Message);\n            }\n        }\n\n        private void Updater_Restart(object sender, EventArgs e) =>\n            WindowManager.AddCallback(new Action(ExitClient), null);\n\n        /// <summary>\n        /// Applies configuration changes (music playback and volume)\n        /// when settings are saved.\n        /// </summary>\n        private void SettingsSaved(object sender, EventArgs e)\n        {\n            if (isMediaPlayerAvailable)\n            {\n                if (MediaPlayer.State == MediaState.Playing)\n                {\n                    if (!UserINISettings.Instance.PlayMainMenuMusic)\n                        isMusicFading = true;\n                }\n                else if (topBar.GetTopMostPrimarySwitchable() == this &&\n                    topBar.LastSwitchType == SwitchType.PRIMARY)\n                {\n                    PlayMusic();\n                }\n            }\n\n            if (!connectionManager.IsConnected)\n                ProgramConstants.PLAYERNAME = UserINISettings.Instance.PlayerName;\n\n            if (UserINISettings.Instance.DiscordIntegration && !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled)\n                discordHandler.Connect();\n            else\n                discordHandler.Disconnect();\n        }\n\n        /// <summary>\n        /// Checks files which are required for the mod to function\n        /// but not distributed with the mod (usually base game files\n        /// for YR mods which can't be standalone).\n        /// </summary>\n        private void CheckRequiredFiles()\n        {\n            List<string> absentFiles = ClientConfiguration.Instance.RequiredFiles.ToList()\n                .FindAll(f => !string.IsNullOrWhiteSpace(f) && !SafePath.GetFile(ProgramConstants.GamePath, f).Exists);\n\n            if (absentFiles.Count > 0)\n            {\n                string description = string.Empty;\n                if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares)\n                {\n                    description = (\"You are missing Yuri's Revenge files that are required\\n\" +\n                        \"to play this mod! Yuri's Revenge mods are not standalone,\\n\" +\n                        \"so you need a copy of following Yuri's Revenge (v.1.001)\\n\" +\n                        \"files placed in the mod folder to play the mod:\").L10N(\"Client:Main:MissingFilesText1Ares\");\n                }\n                else\n                {\n                    description = \"The following required files are missing:\".L10N(\"Client:Main:MissingFilesText1NonAres\");\n                }\n\n                description += Environment.NewLine + Environment.NewLine +\n                    String.Join(Environment.NewLine, absentFiles) +\n                    Environment.NewLine + Environment.NewLine +\n                    \"You won't be able to play without those files.\".L10N(\"Client:Main:MissingFilesText2\");\n\n                XNAMessageBox.Show(WindowManager, \"Missing Files\".L10N(\"Client:Main:MissingFilesTitle\"), description);\n            }\n        }\n\n        private void CheckForbiddenFiles()\n        {\n            List<string> presentFiles = ClientConfiguration.Instance.ForbiddenFiles.ToList()\n                .FindAll(f => !string.IsNullOrWhiteSpace(f) && SafePath.GetFile(ProgramConstants.GamePath, f).Exists);\n\n            if (presentFiles.Count > 0)\n            {\n                string description;\n                if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n                {\n                    description = (\"You have installed the mod on top of a Tiberian Sun\\n\" +\n                    \"copy! This mod is standalone, therefore you have to\\n\" +\n                    \"install it in an empty folder. Otherwise the mod won't\\n\" +\n                    \"function correctly.\\n\\n\" +\n                    \"Please reinstall the mod into an empty folder to play.\").L10N(\"Client:Main:InterferingFilesDetectedTextTS\");\n                }\n                else\n                {\n                    description = \"The following interfering files are present:\".L10N(\"Client:Main:InterferingFilesDetectedTextNonTS1\") +\n                    Environment.NewLine + Environment.NewLine +\n                    String.Join(Environment.NewLine, presentFiles) +\n                    Environment.NewLine + Environment.NewLine +\n                    \"The mod won't work correctly without those files removed.\".L10N(\"Client:Main:InterferingFilesDetectedTextNonTS2\");\n                }\n\n                XNAMessageBox.Show(WindowManager, \"Interfering Files Detected\".L10N(\"Client:Main:InterferingFilesDetectedTitle\"), description);\n            }\n\n        }\n\n        /// <summary>\n        /// Checks whether the client is running for the first time.\n        /// If it is, displays a dialog asking the user if they'd like\n        /// to configure settings.\n        /// </summary>\n        private void CheckIfFirstRun()\n        {\n            if (UserINISettings.Instance.IsFirstRun)\n            {\n                UserINISettings.Instance.IsFirstRun.Value = false;\n                UserINISettings.Instance.SaveSettings();\n\n                firstRunMessageBox = XNAMessageBox.ShowYesNoDialog(WindowManager,\n                    \"Initial Installation\".L10N(\"Client:Main:InitialInstallationTitle\"),\n                    string.Format((\"You have just installed {0}.\\n\" +\n                        \"It's highly recommended that you configure your settings before playing.\\n\" +\n                        \"Do you want to configure them now?\").L10N(\"Client:Main:InitialInstallationText\"),\n                    ClientConfiguration.Instance.LocalGame));\n                firstRunMessageBox.YesClickedAction = FirstRunMessageBox_YesClicked;\n                firstRunMessageBox.NoClickedAction = FirstRunMessageBox_NoClicked;\n            }\n\n            optionsWindow.PostInit();\n        }\n\n        private void CheckAndApplyTranslationGameFiles(bool skipVersionCheck = false)\n        {\n            // In ModMode there is no updater, so always apply translation game files.\n            // Otherwise, skip if already applied for the current game version.\n            if (!skipVersionCheck && !ClientConfiguration.Instance.ModMode &&\n                UserINISettings.Instance.TranslationGameFilesVersion.Value == Updater.GameVersion)\n                return;\n\n            try\n            {\n                Translation.Instance.ApplyTranslationGameFiles();\n                UserINISettings.Instance.TranslationGameFilesVersion.Value = Updater.GameVersion;\n                UserINISettings.Instance.SaveSettings();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Failed to apply translation game files. \" + ex.ToString());\n                XNAMessageBox.Show(WindowManager,\n                    \"Applying Translation Files Failed\".L10N(\"Client:Main:ApplyTranslationFilesFailTitle\"),\n                    \"Applying translation files failed! Error message:\".L10N(\"Client:Main:ApplyTranslationFilesFailText\") + \" \" + ex.Message);\n            }\n        }\n\n        private void FirstRunMessageBox_NoClicked(XNAMessageBox messageBox)\n        {\n            if (customComponentDialogQueued)\n                Updater_OnCustomComponentsOutdated();\n        }\n\n        private void FirstRunMessageBox_YesClicked(XNAMessageBox messageBox) => optionsWindow.Open();\n\n        private void SharedUILogic_GameProcessStarted() => MusicOff();\n\n        private void WindowManager_GameClosing(object sender, EventArgs e) => Clean();\n\n        private void SkirmishLobby_Exited(object sender, EventArgs e)\n        {\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                PlayMusic();\n        }\n\n        private void LanLobby_Exited(object sender, EventArgs e)\n        {\n            topBar.SetLanMode(false);\n\n            if (UserINISettings.Instance.AutomaticCnCNetLogin)\n                connectionManager.Connect();\n\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                PlayMusic();\n        }\n\n        private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCountEventArgs e)\n        {\n            lock (locker)\n            {\n                if (e.PlayerCount == -1)\n                    lblCnCNetPlayerCount.Text = \"N/A\".L10N(\"Client:Main:N/A\");\n                else\n                    lblCnCNetPlayerCount.Text = e.PlayerCount.ToString();\n            }\n        }\n\n        /// <summary>\n        /// Attemps to \"clean\" the client session in a nice way if the user closes the game.\n        /// </summary>\n        private void Clean()\n        {\n            Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated;\n\n            if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel();\n            topBar.Clean();\n            if (UpdateInProgress)\n                Updater.StopUpdate();\n\n            if (connectionManager.IsConnected)\n                connectionManager.Disconnect();\n        }\n\n        /// <summary>\n        /// Starts playing music, initiates an update check if automatic updates\n        /// are enabled and checks whether the client is run for the first time.\n        /// Called after all internal client UI logic has been initialized.\n        /// </summary>\n        public void PostInit()\n        {\n            Logger.Log(\"Main menu post-initialization started.\");\n\n            foreach (XNAControl control in new XNAControl[]\n            {\n                statisticsWindow, // Note: StatisticsWindow must be initialized before any lobbies that extends GameLobbyBase. This is because StatisticsManager is accessed when initializing GameLobbyBase.\n                skirmishLobby,\n                cnCNetGameLoadingLobby,\n                cnCNetGameLobby,\n                cncnetLobby,\n                lanLobby,\n                campaignTagSelector,\n                gameLoadingWindow,\n                updateQueryWindow,\n                manualUpdateQueryWindow,\n                updateWindow,\n                extrasWindow,\n            })\n                DarkeningPanel.AddAndInitializeWithControl(WindowManager, control);\n\n            optionsWindow.SetTopBar(topBar);\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, optionsWindow);\n            WindowManager.AddAndInitializeControl(privateMessagingPanel);\n            privateMessagingPanel.AddChild(privateMessagingWindow);\n            topBar.SetTertiarySwitch(privateMessagingWindow);\n            topBar.SetOptionsWindow(optionsWindow);\n            WindowManager.AddAndInitializeControl(gameInProgressWindow);\n\n            foreach (XNAControl control in new XNAControl[]\n            {\n                skirmishLobby,\n                cnCNetGameLoadingLobby,\n                cnCNetGameLobby,\n                cncnetLobby,\n                lanLobby,\n\n                privateMessagingWindow,\n                optionsWindow,\n\n                campaignTagSelector,\n                gameLoadingWindow,\n                statisticsWindow,\n                updateQueryWindow,\n                manualUpdateQueryWindow,\n                updateWindow,\n                extrasWindow,\n            })\n                control.Disable();\n\n            WindowManager.AddAndInitializeControl(topBar);\n            topBar.AddPrimarySwitchable(this);\n\n            RevertSwitchMainMenuMusicFormat();\n\n            LoadThemeSong();\n\n            PlayMusic();\n\n            if (!ClientConfiguration.Instance.ModMode)\n            {\n                if (Updater.UpdateMirrors.Count < 1)\n                {\n                    lblUpdateStatus.Text = \"No update download mirrors available.\".L10N(\"Client:Main:NoUpdateMirrorsAvailable\");\n                    lblUpdateStatus.DrawUnderline = false;\n                }\n                else if (UserINISettings.Instance.CheckForUpdates)\n                {\n                    CheckForUpdates();\n                }\n                else\n                {\n                    lblUpdateStatus.Text = \"Click to check for updates.\".L10N(\"Client:Main:ClickToCheckUpdate\");\n                }\n            }\n\n            CheckRequiredFiles();\n            CheckForbiddenFiles();\n            CheckIfFirstRun();\n            CheckAndApplyTranslationGameFiles();\n\n            Logger.Log(\"Main menu initialization complete.\");\n            Logger.Log(FormattableString.Invariant($\"Startup complete. Client is ready. Total startup time: {PreStartup.StartupElapsed.TotalSeconds:F3} s.\"));\n\n            MainClientConstants.DisplayErrorAction = (title, error, exit) =>\n            {\n                new XNAMessageBox(WindowManager, title, error, XNAMessageBoxButtons.OK)\n                {\n                    OKClickedAction = _ =>\n                    {\n                        if (exit)\n                            Environment.Exit(1);\n                    },\n\n                }.Show();\n            };\n\n#if ISWINDOWS\n            if (!directDrawWrapperManager.SelectedRenderer.IsDummy)\n                DirectDrawCompatibilityChecker.CheckAndPromptFix(WindowManager);\n#endif\n        }\n\n        private void LoadThemeSong()\n        {\n#if XNA\n            themeSong = AssetLoader.LoadSong(ClientConfiguration.Instance.MainMenuMusicName);\n#else\n\n#if GL\n            string songExtension = \"ogg\";\n#elif DX\n            string songExtension = \"wma\";\n#endif\n\n            FileInfo mainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH,\n                FormattableString.Invariant($\"{ClientConfiguration.Instance.MainMenuMusicName}.{songExtension}\"));\n\n            if (!mainMenuMusicFile.Exists)\n                return;\n\n            try\n            {\n                themeSong = Song.FromUri(ClientConfiguration.Instance.MainMenuMusicName, new Uri(mainMenuMusicFile.FullName));\n            }\n            catch (Exception ex)\n            {\n                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()}\");\n                themeSong = AssetLoader.LoadSong(ClientConfiguration.Instance.MainMenuMusicName);\n            }\n#endif\n        }\n\n        private void RevertSwitchMainMenuMusicFormat()\n        {\n            FileInfo wmaBackupMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH,\n                FormattableString.Invariant($\"{ClientConfiguration.Instance.MainMenuMusicName}.bak\"));\n\n            if (wmaBackupMainMenuMusicFile.Exists)\n            {\n                FileInfo wmaMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH,\n                FormattableString.Invariant($\"{ClientConfiguration.Instance.MainMenuMusicName}.wma\"));\n\n                if (wmaMainMenuMusicFile.Exists)\n                    wmaMainMenuMusicFile.Delete();\n\n                wmaBackupMainMenuMusicFile.MoveTo(wmaMainMenuMusicFile.FullName);\n            }\n        }\n\n        #region Updating / versioning system\n\n        private void UpdateWindow_UpdateFailed(object sender, UpdateFailureEventArgs e)\n        {\n            updateWindow.Disable();\n            lblUpdateStatus.Text = \"Updating failed! Click to retry.\".L10N(\"Client:Main:UpdateFailedClickToRetry\");\n            lblUpdateStatus.DrawUnderline = true;\n            lblUpdateStatus.Enabled = true;\n            UpdateInProgress = false;\n\n            // TODO Enable a dummy Window from DarkeningPanel -- seems not needed. This message box works well.\n            XNAMessageBox msgBox = new XNAMessageBox(WindowManager, \"Update failed\".L10N(\"Client:Main:UpdateFailedTitle\"),\n                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\"),\n                e.Reason, Path.GetFileName(ProgramConstants.StartupExecutable), MainClientConstants.SUPPORT_URL_SHORT), XNAMessageBoxButtons.OK);\n            msgBox.OKClickedAction = (XNAMessageBox messageBox) =>\n            {\n                // TODO Disable the dummy Window from DarkeningPanel -- seems not needed. This message box works well.\n            };\n            msgBox.Show();\n        }\n\n        private void UpdateWindow_UpdateCancelled(object sender, EventArgs e)\n        {\n            updateWindow.Disable();\n            lblUpdateStatus.Text = \"The update was cancelled. Click to retry.\".L10N(\"Client:Main:UpdateCancelledClickToRetry\");\n            lblUpdateStatus.DrawUnderline = true;\n            lblUpdateStatus.Enabled = true;\n            UpdateInProgress = false;\n        }\n\n        private void UpdateWindow_UpdateCompleted(object sender, EventArgs e)\n        {\n            updateWindow.Disable();\n            lblUpdateStatus.Text = string.Format(\"{0} was succesfully updated to v.{1}\".L10N(\"Client:Main:UpdateSuccess\"),\n                MainClientConstants.GAME_NAME_SHORT, Updater.GameVersion);\n            lblVersion.Text = Updater.GameVersion;\n            UpdateInProgress = false;\n            lblUpdateStatus.Enabled = true;\n            lblUpdateStatus.DrawUnderline = false;\n\n            // The update completed without requiring a client restart, so apply\n            // translation game files immediately for the new game version.\n            // (If a restart were required, Updater.Restart fires and the client\n            // exits; the next startup naturally detects the version change.)\n            CheckAndApplyTranslationGameFiles(skipVersionCheck: true);\n        }\n\n        private void LblUpdateStatus_LeftClick(object sender, EventArgs e)\n        {\n            Logger.Log(Updater.VersionState.ToString());\n\n            if (Updater.VersionState == VersionState.OUTDATED ||\n                Updater.VersionState == VersionState.MISMATCHED ||\n                Updater.VersionState == VersionState.UNKNOWN ||\n                Updater.VersionState == VersionState.UPTODATE)\n            {\n                CheckForUpdates();\n            }\n        }\n\n        private void LblVersion_LeftClick(object sender, EventArgs e)\n        {\n            ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.ChangelogURL);\n        }\n\n        private void ForceUpdate()\n        {\n            UpdateInProgress = true;\n            optionsWindow.Disable();\n            updateWindow.ForceUpdate();\n            updateWindow.Enable();\n            lblUpdateStatus.Text = \"Force updating...\".L10N(\"Client:Main:ForceUpdating\");\n        }\n\n        /// <summary>\n        /// Starts a check for updates.\n        /// </summary>\n        private void CheckForUpdates()\n        {\n            if (Updater.UpdateMirrors.Count < 1)\n                return;\n\n            Updater.CheckForUpdates();\n            lblUpdateStatus.Enabled = false;\n            lblUpdateStatus.Text = \"Checking for updates...\"\n                .L10N(\"Client:Main:CheckingForUpdates\");\n            lastUpdateCheckTime = DateTime.Now;\n        }\n\n        private void Updater_FileIdentifiersUpdated()\n            => WindowManager.AddCallback(new Action(HandleFileIdentifierUpdate), null);\n\n        /// <summary>\n        /// Used for displaying the result of an update check in the UI.\n        /// </summary>\n        private void HandleFileIdentifierUpdate()\n        {\n            if (UpdateInProgress)\n            {\n                return;\n            }\n\n            if (Updater.VersionState == VersionState.UPTODATE)\n            {\n                lblUpdateStatus.Text = string.Format(\"{0} is up to date.\".L10N(\"Client:Main:GameUpToDate\"), MainClientConstants.GAME_NAME_SHORT);\n                lblUpdateStatus.Enabled = true;\n                lblUpdateStatus.DrawUnderline = false;\n            }\n            else if (Updater.VersionState == VersionState.OUTDATED && Updater.ManualUpdateRequired)\n            {\n                lblUpdateStatus.Text = \"An update is available. Manual download & installation required.\".L10N(\"Client:Main:UpdateAvailableManualDownloadRequired\");\n                lblUpdateStatus.Enabled = true;\n                lblUpdateStatus.DrawUnderline = false;\n                manualUpdateQueryWindow.SetInfo(Updater.ServerGameVersion, Updater.ManualDownloadURL);\n\n                if (!string.IsNullOrEmpty(Updater.ManualDownloadURL))\n                    manualUpdateQueryWindow.Enable();\n            }\n            else if (Updater.VersionState == VersionState.OUTDATED)\n            {\n                lblUpdateStatus.Text = \"An update is available.\".L10N(\"Client:Main:UpdateAvailable\");\n                updateQueryWindow.SetInfo(Updater.ServerGameVersion, Updater.UpdateSizeInKb);\n                updateQueryWindow.Enable();\n            }\n            else if (Updater.VersionState == VersionState.UNKNOWN)\n            {\n                lblUpdateStatus.Text = \"Checking for updates failed! Click to retry.\".L10N(\"Client:Main:CheckUpdateFailedClickToRetry\");\n                lblUpdateStatus.Enabled = true;\n                lblUpdateStatus.DrawUnderline = true;\n            }\n        }\n\n        /// <summary>\n        /// Asks the user if they'd like to update their custom components.\n        /// Handles an event raised by the updater when it has detected\n        /// that the custom components are out of date.\n        /// </summary>\n        private void Updater_OnCustomComponentsOutdated()\n        {\n            if (updateQueryWindow.Visible)\n                return;\n\n            if (UpdateInProgress)\n                return;\n\n            if ((firstRunMessageBox != null && firstRunMessageBox.Visible) || optionsWindow.Enabled)\n            {\n                // If the custom components are out of date on the first run\n                // or the options window is already open, don't show the dialog\n                customComponentDialogQueued = true;\n                return;\n            }\n\n            customComponentDialogQueued = false;\n\n            XNAMessageBox ccMsgBox = XNAMessageBox.ShowYesNoDialog(WindowManager,\n                \"Custom Component Updates Available\".L10N(\"Client:Main:CustomUpdateAvailableTitle\"),\n                (\"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\"));\n            ccMsgBox.YesClickedAction = CCMsgBox_YesClicked;\n        }\n\n        private void CCMsgBox_YesClicked(XNAMessageBox messageBox)\n        {\n            optionsWindow.Open();\n            optionsWindow.SwitchToCustomComponentsPanel();\n        }\n\n        /// <summary>\n        /// Called when the user has declined an update.\n        /// </summary>\n        private void UpdateQueryWindow_UpdateDeclined(object sender, EventArgs e)\n        {\n            updateQueryWindow.Disable();\n            lblUpdateStatus.Text = \"An update is available, click to install.\".L10N(\"Client:Main:UpdateAvailableClickToInstall\");\n            lblUpdateStatus.Enabled = true;\n            lblUpdateStatus.DrawUnderline = true;\n        }\n\n        /// <summary>\n        /// Called when the user has accepted an update.\n        /// </summary>\n        private void UpdateQueryWindow_UpdateAccepted(object sender, EventArgs e)\n        {\n            updateQueryWindow.Disable();\n            updateWindow.SetData(Updater.ServerGameVersion);\n            updateWindow.Enable();\n            lblUpdateStatus.Text = \"Updating...\".L10N(\"Client:Main:Updating\");\n            UpdateInProgress = true;\n            Updater.StartUpdate();\n        }\n\n        private void ManualUpdateQueryWindow_Closed(object sender, EventArgs e)\n            => manualUpdateQueryWindow.Disable();\n\n        #endregion\n\n        private void BtnOptions_LeftClick(object sender, EventArgs e)\n            => optionsWindow.Open();\n\n        private void BtnNewCampaign_LeftClick(object sender, EventArgs e)\n            => campaignTagSelector.Open();\n\n        private void BtnLoadGame_LeftClick(object sender, EventArgs e)\n            => gameLoadingWindow.Enable();\n\n        private void BtnLan_LeftClick(object sender, EventArgs e)\n        {\n            lanLobby.Open();\n\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                MusicOff();\n\n            if (connectionManager.IsConnected)\n                connectionManager.Disconnect();\n\n            topBar.SetLanMode(true);\n        }\n\n        private void BtnCnCNet_LeftClick(object sender, EventArgs e) => topBar.SwitchToSecondary();\n\n        private void BtnSkirmish_LeftClick(object sender, EventArgs e)\n        {\n            skirmishLobby.Open();\n\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                MusicOff();\n        }\n\n        private void BtnMapEditor_LeftClick(object sender, EventArgs e) => LaunchMapEditor();\n\n        private void BtnStatistics_LeftClick(object sender, EventArgs e) =>\n            statisticsWindow.Enable();\n\n        private void BtnCredits_LeftClick(object sender, EventArgs e)\n        {\n            ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.CreditsURL);\n        }\n\n        private void BtnExtras_LeftClick(object sender, EventArgs e) =>\n            extrasWindow.Enable();\n\n        private void BtnExit_LeftClick(object sender, EventArgs e)\n        {\n#if WINFORMS\n            WindowManager.HideWindow();\n#endif\n            FadeMusicExit();\n        }\n\n        private void SharedUILogic_GameProcessExited() =>\n            AddCallback(new Action(HandleGameProcessExited), null);\n\n        private void HandleGameProcessExited()\n        {\n            gameLoadingWindow.ListSaves();\n            gameLoadingWindow.Disable();\n            gameInProgressWindow.Disable();\n\n            // If music is disabled on menus, check if the main menu is the top-most\n            // window of the top bar and only play music if it is\n            // LAN has the top bar disabled, so to detect the LAN game lobby\n            // we'll check whether the top bar is enabled\n            if (!UserINISettings.Instance.StopMusicOnMenu ||\n                (topBar.Enabled && topBar.LastSwitchType == SwitchType.PRIMARY &&\n                topBar.GetTopMostPrimarySwitchable() == this))\n                PlayMusic();\n        }\n\n        /// <summary>\n        /// Switches to the main menu and performs a check for updates.\n        /// </summary>\n        private void CncnetLobby_UpdateCheck(object sender, EventArgs e)\n        {\n            CheckForUpdates();\n            topBar.SwitchToPrimary();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (isMusicFading)\n                FadeMusic(gameTime);\n\n            base.Update(gameTime);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            lock (locker)\n            {\n                base.Draw(gameTime);\n            }\n        }\n\n        /// <summary>\n        /// Attempts to start playing the menu music.\n        /// </summary>\n        private void PlayMusic()\n        {\n            if (!isMediaPlayerAvailable)\n                return; // SharpDX fails at music playback on Vista\n\n            try\n            {\n                if (themeSong != null && UserINISettings.Instance.PlayMainMenuMusic)\n                {\n                    isMusicFading = false;\n                    MediaPlayer.IsRepeating = true;\n                    MediaPlayer.Volume = (float)UserINISettings.Instance.ClientVolume;\n\n                    MediaPlayer.Play(themeSong);\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Playing main menu music failed! \" + ex.ToString());\n            }\n        }\n\n        /// <summary>\n        /// Lowers the volume of the menu music, or stops playing it if the\n        /// volume is unaudibly low.\n        /// </summary>\n        /// <param name=\"gameTime\">Provides a snapshot of timing values.</param>\n        private void FadeMusic(GameTime gameTime)\n        {\n            if (!isMediaPlayerAvailable || !isMusicFading || themeSong == null)\n                return;\n\n            try\n            {\n                // Fade during 1 second\n                float step = SoundPlayer.Volume * (float)gameTime.ElapsedGameTime.TotalSeconds;\n\n                if (MediaPlayer.Volume > step)\n                    MediaPlayer.Volume -= step;\n                else\n                {\n                    MediaPlayer.Stop();\n                    isMusicFading = false;\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Fading music failed! Message: \" + ex.ToString());\n            }\n        }\n\n        /// <summary>\n        /// Exits the client. Quickly fades the music if it's playing.\n        /// </summary>\n        private void FadeMusicExit()\n        {\n            if (!isMediaPlayerAvailable || themeSong == null)\n            {\n                ExitClient();\n                return;\n            }\n\n            try\n            {\n                float step = MEDIA_PLAYER_VOLUME_EXIT_FADE_STEP * (float)UserINISettings.Instance.ClientVolume;\n\n                if (MediaPlayer.Volume > step)\n                {\n                    MediaPlayer.Volume -= step;\n                    AddCallback(new Action(FadeMusicExit), null);\n                }\n                else\n                {\n                    MediaPlayer.Stop();\n                    ExitClient();\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Fading music on exit failed! Message: \" + ex.ToString());\n            }\n        }\n\n        private void ExitClient()\n        {\n            Logger.Log(\"Exiting.\");\n            WindowManager.CloseGame();\n            themeSong?.Dispose();\n        }\n\n        public void SwitchOn()\n        {\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                PlayMusic();\n\n            if (!ClientConfiguration.Instance.ModMode && UserINISettings.Instance.CheckForUpdates)\n            {\n                // Re-check for updates\n\n                if ((DateTime.Now - lastUpdateCheckTime) > TimeSpan.FromSeconds(UPDATE_RE_CHECK_THRESHOLD))\n                    CheckForUpdates();\n            }\n        }\n\n        public void SwitchOff()\n        {\n            if (UserINISettings.Instance.StopMusicOnMenu)\n                MusicOff();\n        }\n\n        private void MusicOff()\n        {\n            try\n            {\n                if (isMediaPlayerAvailable &&\n                    MediaPlayer.State == MediaState.Playing)\n                {\n                    isMusicFading = true;\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Turning music off failed! Message: \" + ex.ToString());\n            }\n        }\n\n        /// <summary>\n        /// Checks if media player is available currently.\n        /// It is not available on Windows Vista or other systems without the appropriate media player components.\n        /// </summary>\n        /// <returns>True if media player is available, false otherwise.</returns>\n        private bool IsMediaPlayerAvailable()\n        {\n            try\n            {\n                MediaState state = MediaPlayer.State;\n                return true;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Error encountered when checking media player availability. Error message: \" + ex.ToString());\n                return false;\n            }\n        }\n\n        private void LaunchMapEditor()\n        {\n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n            using var mapEditorProcess = new Process();\n\n            if (osVersion != OSVersion.UNIX)\n                mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MapEditorExePath);\n            else\n                mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.UnixMapEditorExePath);\n\n            mapEditorProcess.StartInfo.UseShellExecute = false;\n\n            mapEditorProcess.Start();\n        }\n\n        public string GetSwitchName() => \"Main Menu\".L10N(\"Client:Main:MainMenu\");\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/ManualUpdateQueryWindow.cs",
    "content": "﻿using System;\nusing ClientCore;\nusing ClientCore.Extensions;\nusing ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// A window that redirects users to manually download an update.\n    /// </summary>\n    public class ManualUpdateQueryWindow : XNAWindow\n    {\n        public delegate void ClosedEventHandler(object sender, EventArgs e);\n        public event ClosedEventHandler Closed;\n\n        public ManualUpdateQueryWindow(WindowManager windowManager) : base(windowManager) { }\n\n        private XNALabel lblDescription;\n\n        private string downloadUrl;\n        private string descriptionText;\n\n        public override void Initialize()\n        {\n            Name = \"ManualUpdateQueryWindow\";\n            ClientRectangle = new Rectangle(0, 0, 251, 140);\n            BackgroundTexture = AssetLoader.LoadTexture(\"updatequerybg.png\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0);\n            lblDescription.Text = (\"Version {0} is available.\\n\\nManual download and installation is\\nrequired.\").L10N(\"Client:Main:ManualDownloadAvailable\");\n\n            var btnDownload = new XNAClientButton(WindowManager);\n            btnDownload.Name = nameof(btnDownload);\n            btnDownload.ClientRectangle = new Rectangle(12, 110, 110, 23);\n            btnDownload.Text = \"View Downloads\".L10N(\"Client:Main:ButtonViewDownloads\");\n            btnDownload.LeftClick += BtnDownload_LeftClick;\n\n            var btnClose = new XNAClientButton(WindowManager);\n            btnClose.Name = nameof(btnClose);\n            btnClose.ClientRectangle = new Rectangle(147, 110, 92, 23);\n            btnClose.Text = \"Close\".L10N(\"Client:Main:ButtonClose\");\n            btnClose.LeftClick += BtnClose_LeftClick;\n\n            AddChild(lblDescription);\n            AddChild(btnDownload);\n            AddChild(btnClose);\n\n            base.Initialize();\n\n            // loaded from INI\n            descriptionText = lblDescription.Text;\n\n            CenterOnParent();\n        }\n\n        private void BtnDownload_LeftClick(object sender, EventArgs e)\n            => ProcessLauncher.StartShellProcess(downloadUrl);\n\n        private void BtnClose_LeftClick(object sender, EventArgs e)\n            => Closed?.Invoke(this, e);\n\n        public void SetInfo(string version, string downloadUrl)\n        {\n            this.downloadUrl = downloadUrl;\n            lblDescription.Text = string.Format(descriptionText, version);\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/AudioOptionsPanel.cs",
    "content": "﻿using ClientCore.Extensions;\nusing ClientCore;\nusing ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class AudioOptionsPanel : XNAOptionsPanel\n    {\n        private const int VOLUME_MIN = 0;\n        private const int VOLUME_MAX = 10;\n        private const int VOLUME_SCALE = 10;\n        private const int PADDING_X = 12;\n        private const int PADDING_Y = 14;\n        private const int TRACKBAR_X_PADDING = 16;\n        private const int TRACKBAR_Y_PADDING = 16;\n        private const int TRACKBAR_Y_OFFSET = 2; //trackbars sit slightly higher than their labels.\n        private const int TRACKBAR_HEIGHT = 22;\n        private const int CHECKBOX_SPACING = 4;\n        private const int GROUP_SPACING = 22;\n\n        public AudioOptionsPanel(WindowManager windowManager, UserINISettings iniSettings)\n            : base(windowManager, iniSettings)\n        {\n        }\n\n        private XNATrackbar trbScoreVolume;\n        private XNATrackbar trbSoundVolume;\n        private XNATrackbar trbVoiceVolume;\n\n        private XNALabel lblScoreVolumeValue;\n        private XNALabel lblSoundVolumeValue;\n        private XNALabel lblVoiceVolumeValue;\n\n        private XNAClientCheckBox chkScoreShuffle;\n\n        private XNALabel lblClientVolumeValue;\n        private XNATrackbar trbClientVolume;\n\n        private XNAClientCheckBox chkMainMenuMusic;\n        private XNAClientCheckBox chkStopMusicOnMenu;\n        private XNAClientCheckBox chkStopGameLobbyMessageAudio;\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"AudioOptionsPanel\";\n\n            var lblScoreVolume = new XNALabel(WindowManager);\n            lblScoreVolume.Name = nameof(lblScoreVolume);\n            lblScoreVolume.ClientRectangle = new Rectangle(PADDING_X, PADDING_Y, 0, 0);\n            lblScoreVolume.Text = \"Music Volume:\".L10N(\"Client:DTAConfig:MusicVolume\");\n\n            lblScoreVolumeValue = new XNALabel(WindowManager);\n            lblScoreVolumeValue.Name = nameof(lblScoreVolumeValue);\n            lblScoreVolumeValue.FontIndex = 1;\n            lblScoreVolumeValue.Text = \"0\";\n            lblScoreVolumeValue.ClientRectangle = new Rectangle(\n                Width - lblScoreVolumeValue.Width - PADDING_X,\n                lblScoreVolume.Y, 0, 0);\n\n            trbScoreVolume = new XNATrackbar(WindowManager);\n            trbScoreVolume.Name = nameof(trbScoreVolume);\n            trbScoreVolume.ClientRectangle = new Rectangle(\n                lblScoreVolume.Right + TRACKBAR_X_PADDING,\n                lblScoreVolume.Y - TRACKBAR_Y_OFFSET,\n                lblScoreVolumeValue.X - TRACKBAR_X_PADDING - lblScoreVolume.Right - TRACKBAR_X_PADDING,\n                TRACKBAR_HEIGHT);\n            trbScoreVolume.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2);\n            trbScoreVolume.MinValue = VOLUME_MIN;\n            trbScoreVolume.MaxValue = VOLUME_MAX;\n            trbScoreVolume.ValueChanged += TrbScoreVolume_ValueChanged;\n\n            var lblSoundVolume = new XNALabel(WindowManager);\n            lblSoundVolume.Name = nameof(lblSoundVolume);\n            lblSoundVolume.ClientRectangle = new Rectangle(lblScoreVolume.X,\n                trbScoreVolume.Bottom + TRACKBAR_Y_PADDING + TRACKBAR_Y_OFFSET, 0, 0);\n            lblSoundVolume.Text = \"Sound Volume:\".L10N(\"Client:DTAConfig:SoundVolume\");\n\n            lblSoundVolumeValue = new XNALabel(WindowManager);\n            lblSoundVolumeValue.Name = nameof(lblSoundVolumeValue);\n            lblSoundVolumeValue.FontIndex = 1;\n            lblSoundVolumeValue.Text = \"0\";\n            lblSoundVolumeValue.ClientRectangle = new Rectangle(\n                lblScoreVolumeValue.X,\n                lblSoundVolume.Y, 0, 0);\n\n            trbSoundVolume = new XNATrackbar(WindowManager);\n            trbSoundVolume.Name = nameof(trbSoundVolume);\n            trbSoundVolume.ClientRectangle = new Rectangle(\n                trbScoreVolume.X,\n                trbScoreVolume.Bottom + TRACKBAR_Y_PADDING,\n                trbScoreVolume.Width,\n                trbScoreVolume.Height);\n            trbSoundVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture;\n            trbSoundVolume.MinValue = VOLUME_MIN;\n            trbSoundVolume.MaxValue = VOLUME_MAX;\n            trbSoundVolume.ValueChanged += TrbSoundVolume_ValueChanged;\n\n            var lblVoiceVolume = new XNALabel(WindowManager);\n            lblVoiceVolume.Name = nameof(lblVoiceVolume);\n            lblVoiceVolume.ClientRectangle = new Rectangle(lblScoreVolume.X,\n                trbSoundVolume.Bottom + TRACKBAR_Y_PADDING + TRACKBAR_Y_OFFSET, 0, 0);\n            lblVoiceVolume.Text = \"Voice Volume:\".L10N(\"Client:DTAConfig:VoiceVolume\");\n\n            lblVoiceVolumeValue = new XNALabel(WindowManager);\n            lblVoiceVolumeValue.Name = nameof(lblVoiceVolumeValue);\n            lblVoiceVolumeValue.FontIndex = 1;\n            lblVoiceVolumeValue.Text = \"0\";\n            lblVoiceVolumeValue.ClientRectangle = new Rectangle(\n                lblScoreVolumeValue.X,\n                lblVoiceVolume.Y, 0, 0);\n\n            trbVoiceVolume = new XNATrackbar(WindowManager);\n            trbVoiceVolume.Name = nameof(trbVoiceVolume);\n            trbVoiceVolume.ClientRectangle = new Rectangle(\n                trbScoreVolume.X,\n                trbSoundVolume.Bottom + TRACKBAR_Y_PADDING,\n                trbScoreVolume.Width,\n                trbScoreVolume.Height);\n            trbVoiceVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture;\n            trbVoiceVolume.MinValue = VOLUME_MIN;\n            trbVoiceVolume.MaxValue = VOLUME_MAX;\n            trbVoiceVolume.ValueChanged += TrbVoiceVolume_ValueChanged;\n\n            chkScoreShuffle = new XNAClientCheckBox(WindowManager);\n            chkScoreShuffle.Name = nameof(chkScoreShuffle);\n            chkScoreShuffle.ClientRectangle = new Rectangle(\n                lblScoreVolume.X,\n                trbVoiceVolume.Bottom + TRACKBAR_Y_PADDING, 0, 0);\n            chkScoreShuffle.Text = \"Shuffle Music\".L10N(\"Client:DTAConfig:ShuffleMusic\");\n            AddChild(chkScoreShuffle);\n\n            var lblClientVolume = new XNALabel(WindowManager);\n            lblClientVolume.Name = nameof(lblClientVolume);\n            lblClientVolume.ClientRectangle = new Rectangle(lblScoreVolume.X,\n                chkScoreShuffle.Bottom + GROUP_SPACING + TRACKBAR_Y_OFFSET, 0, 0);\n            lblClientVolume.Text = \"Client Volume:\".L10N(\"Client:DTAConfig:ClientVolume\");\n\n            lblClientVolumeValue = new XNALabel(WindowManager);\n            lblClientVolumeValue.Name = nameof(lblClientVolumeValue);\n            lblClientVolumeValue.FontIndex = 1;\n            lblClientVolumeValue.Text = \"0\";\n            lblClientVolumeValue.ClientRectangle = new Rectangle(\n                lblScoreVolumeValue.X,\n                lblClientVolume.Y, 0, 0);\n\n            trbClientVolume = new XNATrackbar(WindowManager);\n            trbClientVolume.Name = nameof(trbClientVolume);\n            trbClientVolume.ClientRectangle = new Rectangle(\n                trbScoreVolume.X,\n                lblClientVolume.Y - TRACKBAR_Y_OFFSET,\n                trbScoreVolume.Width,\n                trbScoreVolume.Height);\n            trbClientVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture;\n            trbClientVolume.MinValue = VOLUME_MIN;\n            trbClientVolume.MaxValue = VOLUME_MAX;\n            trbClientVolume.ValueChanged += TrbClientVolume_ValueChanged;\n\n            chkMainMenuMusic = new XNAClientCheckBox(WindowManager);\n            chkMainMenuMusic.Name = nameof(chkMainMenuMusic);\n            chkMainMenuMusic.Text = \"Main menu music\".L10N(\"Client:DTAConfig:MainMenuMusic\");\n            chkMainMenuMusic.ClientRectangle = new Rectangle(\n                lblScoreVolume.X,\n                trbClientVolume.Bottom + PADDING_Y, 0, 0);\n            chkMainMenuMusic.CheckedChanged += ChkMainMenuMusic_CheckedChanged;\n            AddChild(chkMainMenuMusic);\n\n            chkStopMusicOnMenu = new XNAClientCheckBox(WindowManager);\n            chkStopMusicOnMenu.Name = nameof(chkStopMusicOnMenu);\n            chkStopMusicOnMenu.Text = \"Don't play main menu music in lobbies\".L10N(\"Client:DTAConfig:NoLobbiesMusic\");\n            chkStopMusicOnMenu.ClientRectangle = new Rectangle(\n                lblScoreVolume.X, chkMainMenuMusic.ClientRectangle.Bottom + CHECKBOX_SPACING, 0, 0);\n            AddChild(chkStopMusicOnMenu);\n\n            chkStopGameLobbyMessageAudio = new XNAClientCheckBox(WindowManager);\n            chkStopGameLobbyMessageAudio.Name = nameof(chkStopGameLobbyMessageAudio);\n            chkStopGameLobbyMessageAudio.Text = \"Don't play lobby message audio when game is running\".L10N(\"Client:DTAConfig:NoGameLobbyMessageAudio\");\n            chkStopGameLobbyMessageAudio.ClientRectangle = new Rectangle(\n                lblScoreVolume.X, chkStopMusicOnMenu.Bottom + CHECKBOX_SPACING, 0, 0);\n            AddChild(chkStopGameLobbyMessageAudio);\n\n            AddChild(lblScoreVolume);\n            AddChild(lblScoreVolumeValue);\n            AddChild(trbScoreVolume);\n            AddChild(lblSoundVolume);\n            AddChild(lblSoundVolumeValue);\n            AddChild(trbSoundVolume);\n            AddChild(lblVoiceVolume);\n            AddChild(lblVoiceVolumeValue);\n            AddChild(trbVoiceVolume);\n            AddChild(lblClientVolume);\n            AddChild(lblClientVolumeValue);\n            AddChild(trbClientVolume);\n\n            WindowManager.SoundPlayer.SetVolume(trbClientVolume.Value / (float)VOLUME_SCALE);\n        }\n\n        private void ChkMainMenuMusic_CheckedChanged(object sender, EventArgs e)\n        {\n            chkStopMusicOnMenu.AllowChecking = chkMainMenuMusic.Checked;\n            chkStopMusicOnMenu.Checked = chkMainMenuMusic.Checked;\n        }\n\n        private void TrbScoreVolume_ValueChanged(object sender, EventArgs e)\n        {\n            lblScoreVolumeValue.Text = trbScoreVolume.Value.ToString();\n        }\n\n        private void TrbSoundVolume_ValueChanged(object sender, EventArgs e)\n        {\n            lblSoundVolumeValue.Text = trbSoundVolume.Value.ToString();\n        }\n\n        private void TrbVoiceVolume_ValueChanged(object sender, EventArgs e)\n        {\n            lblVoiceVolumeValue.Text = trbVoiceVolume.Value.ToString();\n        }\n\n        private void TrbClientVolume_ValueChanged(object sender, EventArgs e)\n        {\n            lblClientVolumeValue.Text = trbClientVolume.Value.ToString();\n            WindowManager.SoundPlayer.SetVolume(trbClientVolume.Value / (float)VOLUME_SCALE);\n        }\n\n        public override void Load()\n        {\n            base.Load();\n\n            trbScoreVolume.Value = (int)(IniSettings.ScoreVolume * VOLUME_SCALE);\n            trbSoundVolume.Value = (int)(IniSettings.SoundVolume * VOLUME_SCALE);\n            trbVoiceVolume.Value = (int)(IniSettings.VoiceVolume * VOLUME_SCALE);\n\n            chkScoreShuffle.Checked = IniSettings.IsScoreShuffle;\n\n            trbClientVolume.Value = (int)(IniSettings.ClientVolume * VOLUME_SCALE);\n\n            chkMainMenuMusic.Checked = IniSettings.PlayMainMenuMusic;\n            chkStopMusicOnMenu.Checked = IniSettings.StopMusicOnMenu;\n            chkStopGameLobbyMessageAudio.Checked = IniSettings.StopGameLobbyMessageAudio;\n        }\n\n        public override bool Save()\n        {\n            bool restartRequired = base.Save();\n\n            IniSettings.ScoreVolume.Value = trbScoreVolume.Value / (double)VOLUME_SCALE;\n            IniSettings.SoundVolume.Value = trbSoundVolume.Value / (double)VOLUME_SCALE;\n            IniSettings.VoiceVolume.Value = trbVoiceVolume.Value / (double)VOLUME_SCALE;\n\n            IniSettings.IsScoreShuffle.Value = chkScoreShuffle.Checked;\n\n            IniSettings.ClientVolume.Value = trbClientVolume.Value / (double)VOLUME_SCALE;\n\n            IniSettings.PlayMainMenuMusic.Value = chkMainMenuMusic.Checked;\n            IniSettings.StopMusicOnMenu.Value = chkStopMusicOnMenu.Checked;\n            IniSettings.StopGameLobbyMessageAudio.Value = chkStopGameLobbyMessageAudio.Checked;\n\n            return restartRequired;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/CnCNetOptionsPanel.cs",
    "content": "﻿using ClientCore.Extensions;\nusing ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing ClientCore.Enums;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class CnCNetOptionsPanel : XNAOptionsPanel\n    {\n        public CnCNetOptionsPanel(WindowManager windowManager, UserINISettings iniSettings,\n            GameCollection gameCollection)\n            : base(windowManager, iniSettings)\n        {\n            this.gameCollection = gameCollection;\n        }\n\n        XNAClientCheckBox chkPingUnofficialTunnels;\n        XNAClientCheckBox chkWriteInstallPathToRegistry;\n        XNAClientCheckBox chkPlaySoundOnGameHosted;\n\n        XNAClientCheckBox chkNotifyOnUserListChange;\n\n        XNAClientCheckBox chkSkipLoginWindow;\n        XNAClientCheckBox chkPersistentMode;\n        XNAClientCheckBox chkConnectOnStartup;\n        XNAClientCheckBox chkDiscordIntegration;\n        XNAClientCheckBox chkSteamIntegration;\n        XNAClientCheckBox chkAllowGameInvitesFromFriendsOnly;\n        XNAClientCheckBox chkDisablePrivateMessagePopup;\n\n        XNAClientDropDown ddAllowPrivateMessagesFrom;\n\n        GameCollection gameCollection;\n\n        List<XNAClientCheckBox> followedGameChks = new List<XNAClientCheckBox>();\n\n        public override void Initialize()\n        {\n            base.Initialize();\n            Name = \"CnCNetOptionsPanel\";\n\n            InitOptions();\n            InitGameListPanel();\n        }\n\n        private void InitOptions()\n        {\n            // LEFT COLUMN\n\n            chkPingUnofficialTunnels = new XNAClientCheckBox(WindowManager);\n            chkPingUnofficialTunnels.Name = nameof(chkPingUnofficialTunnels);\n            chkPingUnofficialTunnels.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            chkPingUnofficialTunnels.Text = \"Ping unofficial CnCNet tunnels\".L10N(\"Client:DTAConfig:PingUnofficial\");\n\n            AddChild(chkPingUnofficialTunnels);\n\n            chkWriteInstallPathToRegistry = new XNAClientCheckBox(WindowManager);\n            chkWriteInstallPathToRegistry.Name = nameof(chkWriteInstallPathToRegistry);\n            chkWriteInstallPathToRegistry.ClientRectangle = new Rectangle(\n                chkPingUnofficialTunnels.X,\n                chkPingUnofficialTunnels.Bottom + 12, 0, 0);\n            chkWriteInstallPathToRegistry.Text = (\"Write game installation path to Windows\\n\" +\n                \"Registry (makes it possible to join\\n\" +\n                 \"other games' game rooms on CnCNet)\").L10N(\"Client:DTAConfig:WriteGameRegistry\");\n\n            AddChild(chkWriteInstallPathToRegistry);\n\n            chkPlaySoundOnGameHosted = new XNAClientCheckBox(WindowManager);\n            chkPlaySoundOnGameHosted.Name = nameof(chkPlaySoundOnGameHosted);\n            chkPlaySoundOnGameHosted.ClientRectangle = new Rectangle(\n                chkPingUnofficialTunnels.X,\n                chkWriteInstallPathToRegistry.Bottom + 12, 0, 0);\n            chkPlaySoundOnGameHosted.Text = \"Play sound when a game is hosted\".L10N(\"Client:DTAConfig:PlaySoundGameHosted\");\n\n            AddChild(chkPlaySoundOnGameHosted);\n\n            chkNotifyOnUserListChange = new XNAClientCheckBox(WindowManager);\n            chkNotifyOnUserListChange.Name = nameof(chkNotifyOnUserListChange);\n            chkNotifyOnUserListChange.ClientRectangle = new Rectangle(\n                chkPingUnofficialTunnels.X,\n                chkPlaySoundOnGameHosted.Bottom + 12, 0, 0);\n            chkNotifyOnUserListChange.Text = (\"Show player join / quit messages\\n\" +\n                \"on CnCNet lobby\").L10N(\"Client:DTAConfig:ShowPlayerJoinQuit\");\n\n            AddChild(chkNotifyOnUserListChange);\n\n            chkDisablePrivateMessagePopup = new XNAClientCheckBox(WindowManager);\n            chkDisablePrivateMessagePopup.Name = nameof(chkDisablePrivateMessagePopup);\n            chkDisablePrivateMessagePopup.ClientRectangle = new Rectangle(\n                chkNotifyOnUserListChange.X,\n                chkNotifyOnUserListChange.Bottom + 8, 0, 0);\n            chkDisablePrivateMessagePopup.Text = \"Disable Popups from Private Messages\".L10N(\"Client:DTAConfig:DisablePMPopup\");\n\n            AddChild(chkDisablePrivateMessagePopup);\n\n            InitAllowPrivateMessagesFromDropdown();\n\n            // RIGHT COLUMN\n\n            chkSkipLoginWindow = new XNAClientCheckBox(WindowManager);\n            chkSkipLoginWindow.Name = nameof(chkSkipLoginWindow);\n            chkSkipLoginWindow.ClientRectangle = new Rectangle(\n                276,\n                12, 0, 0);\n            chkSkipLoginWindow.Text = \"Skip login dialog\".L10N(\"Client:DTAConfig:SkipLoginDialog\");\n            chkSkipLoginWindow.CheckedChanged += ChkSkipLoginWindow_CheckedChanged;\n\n            AddChild(chkSkipLoginWindow);\n\n            chkPersistentMode = new XNAClientCheckBox(WindowManager);\n            chkPersistentMode.Name = nameof(chkPersistentMode);\n            chkPersistentMode.ClientRectangle = new Rectangle(\n                chkSkipLoginWindow.X,\n                chkSkipLoginWindow.Bottom + 12, 0, 0);\n            chkPersistentMode.Text = \"Stay connected outside of the CnCNet lobby\".L10N(\"Client:DTAConfig:StayConnect\");\n            chkPersistentMode.CheckedChanged += ChkPersistentMode_CheckedChanged;\n\n            AddChild(chkPersistentMode);\n\n            chkConnectOnStartup = new XNAClientCheckBox(WindowManager);\n            chkConnectOnStartup.Name = nameof(chkConnectOnStartup);\n            chkConnectOnStartup.ClientRectangle = new Rectangle(\n                chkSkipLoginWindow.X,\n                chkPersistentMode.Bottom + 12, 0, 0);\n            chkConnectOnStartup.Text = \"Connect automatically on client startup\".L10N(\"Client:DTAConfig:ConnectOnStart\");\n            chkConnectOnStartup.AllowChecking = false;\n\n            AddChild(chkConnectOnStartup);\n\n            chkDiscordIntegration = new XNAClientCheckBox(WindowManager);\n            chkDiscordIntegration.Name = nameof(chkDiscordIntegration);\n            chkDiscordIntegration.ClientRectangle = new Rectangle(\n                chkSkipLoginWindow.X,\n                chkConnectOnStartup.Bottom + 12, 0, 0);\n            chkDiscordIntegration.Text = \"Show detailed game info in Discord status\".L10N(\"Client:DTAConfig:DiscordStatus\");\n\n            if (ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled)\n            {\n                chkDiscordIntegration.AllowChecking = false;\n                chkDiscordIntegration.Checked = false;\n            }\n            else\n            {\n                chkDiscordIntegration.AllowChecking = true;\n            }\n\n            AddChild(chkDiscordIntegration);\n\n            chkAllowGameInvitesFromFriendsOnly = new XNAClientCheckBox(WindowManager);\n            chkAllowGameInvitesFromFriendsOnly.Name = nameof(chkAllowGameInvitesFromFriendsOnly);\n            chkAllowGameInvitesFromFriendsOnly.ClientRectangle = new Rectangle(\n                chkDiscordIntegration.X,\n                chkDiscordIntegration.Bottom + 12, 0, 0);\n            chkAllowGameInvitesFromFriendsOnly.Text = \"Only receive game invitations from friends\".L10N(\"Client:DTAConfig:FriendsOnly\");\n\n            AddChild(chkAllowGameInvitesFromFriendsOnly);\n\n\n            chkSteamIntegration = new XNAClientCheckBox(WindowManager);\n            chkSteamIntegration.Name = nameof(chkSteamIntegration);\n            chkSteamIntegration.ClientRectangle = new Rectangle(\n                chkAllowGameInvitesFromFriendsOnly.X,\n                chkAllowGameInvitesFromFriendsOnly.Bottom + 12, 0, 0);\n            chkSteamIntegration.Text = \"Show the game being played in Steam\".L10N(\"Client:DTAConfig:SteamStatus\");\n\n            AddChild(chkSteamIntegration);\n        }\n\n        private void InitAllowPrivateMessagesFromDropdown()\n        {\n            XNALabel lblAllPrivateMessagesFrom = new XNALabel(WindowManager);\n            lblAllPrivateMessagesFrom.Name = nameof(lblAllPrivateMessagesFrom);\n            lblAllPrivateMessagesFrom.Text = \"Allow Private Messages From:\".L10N(\"Client:DTAConfig:AllowPMFrom\");\n            lblAllPrivateMessagesFrom.ClientRectangle = new Rectangle(\n                chkDisablePrivateMessagePopup.X,\n                chkDisablePrivateMessagePopup.Bottom + 12, 165, 0);\n\n            AddChild(lblAllPrivateMessagesFrom);\n\n            ddAllowPrivateMessagesFrom = new XNAClientDropDown(WindowManager);\n            ddAllowPrivateMessagesFrom.Name = nameof(ddAllowPrivateMessagesFrom);\n            ddAllowPrivateMessagesFrom.ClientRectangle = new Rectangle(\n                lblAllPrivateMessagesFrom.Right,\n                lblAllPrivateMessagesFrom.Y - 2, 110, 0);\n\n            ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem()\n            {\n                Text = \"All\".L10N(\"Client:DTAConfig:PMAll\"),\n                Tag = AllowPrivateMessagesFromEnum.All,\n            });\n\n            ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem()\n            {\n                Text = \"Current channel\".L10N(\"Client:DTAConfig:PMCurrentChannel\"),\n                Tag = AllowPrivateMessagesFromEnum.CurrentChannel,\n            });\n\n            ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem()\n            {\n                Text = \"Friends\".L10N(\"Client:DTAConfig:PMFriends\"),\n                Tag = AllowPrivateMessagesFromEnum.Friends,\n            });\n\n            ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem()\n            {\n                Text = \"None\".L10N(\"Client:DTAConfig:PMNone\"),\n                Tag = AllowPrivateMessagesFromEnum.None,\n            });\n\n            AddChild(ddAllowPrivateMessagesFrom);\n        }\n\n        private void InitGameListPanel()\n        {\n            const int gameListPanelHeight = 185;\n            XNAPanel gameListPanel = new XNAPanel(WindowManager);\n            gameListPanel.DrawBorders = false;\n            gameListPanel.Name = nameof(gameListPanel);\n            gameListPanel.ClientRectangle = new Rectangle(0, Bottom - gameListPanelHeight, Width, gameListPanelHeight);\n\n            AddChild(gameListPanel);\n\n            var lblFollowedGames = new XNALabel(WindowManager);\n            lblFollowedGames.Name = nameof(lblFollowedGames);\n            lblFollowedGames.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            lblFollowedGames.Text = \"Show game rooms from the following games:\".L10N(\"Client:DTAConfig:ShowRoomFromGame\");\n\n            gameListPanel.AddChild(lblFollowedGames);\n\n            // Max number of games per column\n            const int maxGamesPerColumn = 4;\n            // Spacing buffer between columns\n            const int columnBuffer = 20;\n            // Spacing buffer between rows\n            const int rowBuffer = 22;\n            // Render width of a game icon\n            const int gameIconWidth = 16;\n            // Spacing buffer between game icon and game check box\n            const int gameIconBuffer = 6;\n\n            // List of supported games\n            IEnumerable<CnCNetGame> supportedGames = gameCollection.GameList\n                .Where(game => game.Supported && !string.IsNullOrEmpty(game.GameBroadcastChannel));\n\n            // Convert to a matrix of XNAPanels that contain the game icons and check boxes\n            List<List<XNAPanel>> gamePanelMatrix = supportedGames\n                .Select(game =>\n                {\n                    var gameIconPanel = new XNAPanel(WindowManager);\n                    gameIconPanel.Name = \"gameIcon\" + game.InternalName.ToUpperInvariant();\n                    gameIconPanel.ClientRectangle = new Rectangle(0, 0, gameIconWidth, gameIconWidth);\n                    gameIconPanel.DrawBorders = false;\n                    gameIconPanel.BackgroundTexture = game.Texture;\n\n                    var gameChkBox = new XNAClientCheckBox(WindowManager);\n                    gameChkBox.Name = game.InternalName.ToUpperInvariant();\n                    gameChkBox.ClientRectangle = new Rectangle(gameIconPanel.Right + gameIconBuffer, 0, 0, 0);\n                    gameChkBox.Text = game.UIName;\n\n                    var gamePanel = new XNAPanel(WindowManager);\n                    gamePanel.AddChild(gameIconPanel);\n                    gamePanel.AddChild(gameChkBox);\n                    gamePanel.Name = \"gamePanel\" + game.InternalName.ToUpperInvariant();\n                    gamePanel.DrawBorders = false;\n                    gamePanel.ClientRectangle = new Rectangle(lblFollowedGames.X, 0, gameIconPanel.Width + gameChkBox.Width + gameIconBuffer, gameIconPanel.Height);\n\n                    followedGameChks.Add(gameChkBox);\n                    return gamePanel;\n                })\n                .ToMatrix(maxGamesPerColumn);\n\n\n            // Calculate max widths for each column\n            List<int> columnWidths = gamePanelMatrix\n                .Select(columnList => columnList.Max(gamePanel => gamePanel.Children.Last().Right + columnBuffer))\n                .ToList();\n\n            // Reposition each game panel and then add them to the overall list panel\n            int startY = lblFollowedGames.Bottom + 12;\n            for (int col = 0; col < gamePanelMatrix.Count; col++)\n            {\n                List<XNAPanel> gamePanelColumn = gamePanelMatrix[col];\n                for (int row = 0; row < gamePanelColumn.Count; row++)\n                {\n                    int columnOffset = columnWidths.Take(col).Sum();\n                    int rowOffset = startY + row * rowBuffer;\n                    XNAPanel gamePanel = gamePanelColumn[row];\n                    gamePanel.ClientRectangle = new Rectangle(gamePanel.X + columnOffset, rowOffset, gamePanel.Width, gamePanel.Height);\n                    gameListPanel.AddChild(gamePanel);\n                }\n            }\n        }\n\n        private void ChkSkipLoginWindow_CheckedChanged(object sender, EventArgs e)\n        {\n            CheckConnectOnStartupAllowance();\n        }\n\n        private void ChkPersistentMode_CheckedChanged(object sender, EventArgs e)\n        {\n            CheckConnectOnStartupAllowance();\n        }\n\n        private void CheckConnectOnStartupAllowance()\n        {\n            if (!chkSkipLoginWindow.Checked || !chkPersistentMode.Checked)\n            {\n                chkConnectOnStartup.AllowChecking = false;\n                chkConnectOnStartup.Checked = false;\n                return;\n            }\n\n            chkConnectOnStartup.AllowChecking = true;\n        }\n\n        public override void Load()\n        {\n            base.Load();\n\n            chkPingUnofficialTunnels.Checked = IniSettings.PingUnofficialCnCNetTunnels;\n            chkWriteInstallPathToRegistry.Checked = IniSettings.WritePathToRegistry;\n            chkPlaySoundOnGameHosted.Checked = IniSettings.PlaySoundOnGameHosted;\n            chkNotifyOnUserListChange.Checked = IniSettings.NotifyOnUserListChange;\n            chkDisablePrivateMessagePopup.Checked = IniSettings.DisablePrivateMessagePopups;\n            SetAllowPrivateMessagesFromState(IniSettings.AllowPrivateMessagesFromState);\n            chkConnectOnStartup.Checked = IniSettings.AutomaticCnCNetLogin;\n            chkSkipLoginWindow.Checked = IniSettings.SkipConnectDialog;\n            chkPersistentMode.Checked = IniSettings.PersistentMode;\n            chkSteamIntegration.Checked = IniSettings.SteamIntegration;\n\n            chkDiscordIntegration.Checked = !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled\n                && IniSettings.DiscordIntegration;\n\n            chkAllowGameInvitesFromFriendsOnly.Checked = IniSettings.AllowGameInvitesFromFriendsOnly;\n\n            string localGame = ClientConfiguration.Instance.LocalGame.ToUpperInvariant();\n\n            foreach (var chkBox in followedGameChks)\n            {\n                if (chkBox.Name == localGame)\n                {\n                    chkBox.AllowChecking = false;\n                    chkBox.Checked = true;\n                    IniSettings.SettingsIni.SetBooleanValue(\"Channels\", localGame, true);\n                    continue;\n                }\n\n                chkBox.Checked = IniSettings.IsGameFollowed(chkBox.Name);\n            }\n        }\n\n        public override bool Save()\n        {\n            bool restartRequired = base.Save();\n\n            IniSettings.PingUnofficialCnCNetTunnels.Value = chkPingUnofficialTunnels.Checked;\n            IniSettings.WritePathToRegistry.Value = chkWriteInstallPathToRegistry.Checked;\n            IniSettings.PlaySoundOnGameHosted.Value = chkPlaySoundOnGameHosted.Checked;\n            IniSettings.NotifyOnUserListChange.Value = chkNotifyOnUserListChange.Checked;\n            IniSettings.DisablePrivateMessagePopups.Value = chkDisablePrivateMessagePopup.Checked;\n            IniSettings.AllowPrivateMessagesFromState.Value = GetAllowPrivateMessagesFromState();\n            IniSettings.AutomaticCnCNetLogin.Value = chkConnectOnStartup.Checked;\n            IniSettings.SkipConnectDialog.Value = chkSkipLoginWindow.Checked;\n            IniSettings.PersistentMode.Value = chkPersistentMode.Checked;\n            IniSettings.SteamIntegration.Value = chkSteamIntegration.Checked;\n\n            if (!ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled)\n            {\n                IniSettings.DiscordIntegration.Value = chkDiscordIntegration.Checked;\n            }\n\n            IniSettings.AllowGameInvitesFromFriendsOnly.Value = chkAllowGameInvitesFromFriendsOnly.Checked;\n\n            foreach (var chkBox in followedGameChks)\n            {\n                IniSettings.SettingsIni.SetBooleanValue(\"Channels\", chkBox.Name, chkBox.Checked);\n            }\n\n            return restartRequired;\n        }\n\n        private void SetAllowPrivateMessagesFromState(int state)\n        {\n            var selectedIndex = ddAllowPrivateMessagesFrom.Items.FindIndex(i => (int)i.Tag == state);\n            if (selectedIndex < 0)\n                selectedIndex = ddAllowPrivateMessagesFrom.Items.FindIndex(i => (AllowPrivateMessagesFromEnum)i.Tag == AllowPrivateMessagesFromEnum.All);\n\n            ddAllowPrivateMessagesFrom.SelectedIndex = selectedIndex;\n        }\n\n        private int GetAllowPrivateMessagesFromState()\n        {\n            return (int)(ddAllowPrivateMessagesFrom.SelectedItem?.Tag ?? AllowPrivateMessagesFromEnum.All);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/ComponentsPanel.cs",
    "content": "using ClientCore.Extensions;\nusing ClientCore;\nusing ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing ClientUpdater;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class ComponentsPanel : XNAOptionsPanel\n    {\n        public ComponentsPanel(WindowManager windowManager, UserINISettings iniSettings)\n            : base(windowManager, iniSettings)\n        {\n        }\n\n        List<XNAClientButton> installationButtons = new List<XNAClientButton>();\n\n        bool downloadCancelled = false;\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"ComponentsPanel\";\n\n            int componentIndex = 0;\n\n            if (Updater.CustomComponents == null)\n                return;\n\n            foreach (CustomComponent c in Updater.CustomComponents)\n            {\n                string buttonText = \"Not Available\".L10N(\"Client:DTAConfig:NotAvailable\");\n\n                if (SafePath.GetFile(ProgramConstants.GamePath, c.LocalPath).Exists)\n                {\n                    buttonText = \"Uninstall\".L10N(\"Client:DTAConfig:Uninstall\");\n\n                    if (c.LocalIdentifier != c.RemoteIdentifier)\n                        buttonText = \"Update\".L10N(\"Client:DTAConfig:Update\");\n                }\n                else\n                {\n                    if (!string.IsNullOrEmpty(c.RemoteIdentifier))\n                        buttonText = \"Install\".L10N(\"Client:DTAConfig:Install\");\n                }\n\n                XNAClientButton btn = new(WindowManager)\n                {\n                    Name = \"btn\" + c.ININame,\n                    ClientRectangle = new Rectangle(Width - 145,\n                        12 + componentIndex * 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT),\n                    Text = buttonText,\n                    Tag = c\n                };\n                btn.LeftClick += Btn_LeftClick;\n\n                XNALabel lbl = new(WindowManager)\n                {\n                    Name = \"lbl\" + c.ININame,\n                    ClientRectangle = new Rectangle(12, btn.Y + 2, 0, 0),\n                    Text = c.GUIName\n                };\n\n                AddChild(btn);\n                AddChild(lbl);\n\n                installationButtons.Add(btn);\n\n                componentIndex++;\n            }\n\n            Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated;\n        }\n\n        private void Updater_FileIdentifiersUpdated()\n            => UpdateInstallationButtons();\n\n        public override void Load()\n        {\n            base.Load();\n\n            UpdateInstallationButtons();\n        }\n\n        private void UpdateInstallationButtons()\n        {\n            if (Updater.CustomComponents == null)\n                return;\n\n            int componentIndex = 0;\n\n            foreach (CustomComponent c in Updater.CustomComponents)\n            {\n                if (!c.Initialized || c.IsBeingDownloaded)\n                {\n                    installationButtons[componentIndex].AllowClick = false;\n                    componentIndex++;\n                    continue;\n                }\n\n                string buttonText = \"Not Available\".L10N(\"Client:DTAConfig:NotAvailable\");\n                bool buttonEnabled = false;\n\n                if (SafePath.GetFile(ProgramConstants.GamePath, c.LocalPath).Exists)\n                {\n                    buttonText = \"Uninstall\".L10N(\"Client:DTAConfig:Uninstall\");\n                    buttonEnabled = true;\n\n                    if (c.LocalIdentifier != c.RemoteIdentifier)\n                        buttonText = \"Update\".L10N(\"Client:DTAConfig:Update\") + $\" ({GetSizeString(c.RemoteSize)})\";\n                }\n                else\n                {\n                    if (!string.IsNullOrEmpty(c.RemoteIdentifier))\n                    {\n                        buttonText = \"Install\".L10N(\"Client:DTAConfig:Install\") + $\" ({GetSizeString(c.RemoteSize)})\";\n                        buttonEnabled = true;\n                    }\n                }\n\n                installationButtons[componentIndex].Text = buttonText;\n                installationButtons[componentIndex].AllowClick = buttonEnabled;\n\n                componentIndex++;\n            }\n        }\n\n        private void Btn_LeftClick(object sender, EventArgs e)\n        {\n            var btn = (XNAClientButton)sender;\n\n            var cc = (CustomComponent)btn.Tag;\n\n            if (cc.IsBeingDownloaded)\n                return;\n\n            FileInfo localFileInfo = SafePath.GetFile(ProgramConstants.GamePath, cc.LocalPath);\n\n            if (localFileInfo.Exists)\n            {\n                if (cc.LocalIdentifier == cc.RemoteIdentifier)\n                {\n                    localFileInfo.IsReadOnly = false;\n                    localFileInfo.Delete();\n                    btn.Text = \"Install\".L10N(\"Client:DTAConfig:Install\") + $\" ({GetSizeString(cc.RemoteSize)})\";\n                    return;\n                }\n\n                btn.AllowClick = false;\n\n                cc.DownloadFinished += cc_DownloadFinished;\n                cc.DownloadProgressChanged += cc_DownloadProgressChanged;\n                cc.DownloadComponent();\n            }\n            else\n            {\n                var msgBox = new XNAMessageBox(WindowManager, \"Confirmation Required\".L10N(\"Client:DTAConfig:UpdateConfirmRequiredTitle\"),\n                    string.Format((\"To enable {0} the Client will need to download the necessary files to your game directory.\\n\\n\" +\n                        \"This will take an additional {1} of disk space, and the download may take some time\\n\" +\n                        \"depending on your Internet connection speed. The size of the download is {2}.\\n\\n\" +\n                        \"You will not be able to play during the download. Do you wish to continue?\").L10N(\"Client:DTAConfig:UpdateConfirmRequiredText\"),\n                        cc.GUIName, GetSizeString(cc.RemoteSize), GetSizeString(cc.Archived ? cc.RemoteArchiveSize : cc.RemoteSize)),\n                    XNAMessageBoxButtons.YesNo);\n\n                msgBox.Tag = btn;\n                msgBox.Show();\n                msgBox.YesClickedAction = MsgBox_YesClicked;\n            }\n        }\n\n        private void MsgBox_YesClicked(XNAMessageBox messageBox)\n        {\n            var btn = (XNAClientButton)messageBox.Tag;\n            btn.AllowClick = false;\n\n            var cc = (CustomComponent)btn.Tag;\n\n            cc.DownloadFinished += cc_DownloadFinished;\n            cc.DownloadProgressChanged += cc_DownloadProgressChanged;\n            cc.DownloadComponent();\n        }\n\n        public void InstallComponent(int id)\n        {\n            var btn = installationButtons[id];\n            btn.AllowClick = false;\n\n            var cc = (CustomComponent)btn.Tag;\n\n            cc.DownloadFinished += cc_DownloadFinished;\n            cc.DownloadProgressChanged += cc_DownloadProgressChanged;\n            cc.DownloadComponent();\n        }\n\n        /// <summary>\n        /// Called whenever a custom component download's progress is changed.\n        /// </summary>\n        /// <param name=\"c\">The CustomComponent object.</param>\n        /// <param name=\"percentage\">The current download progress percentage.</param>\n        private void cc_DownloadProgressChanged(CustomComponent c, int percentage)\n        {\n            WindowManager.AddCallback(new Action<CustomComponent, int>(HandleDownloadProgressChanged), c, percentage);\n        }\n\n        private void HandleDownloadProgressChanged(CustomComponent cc, int percentage)\n        {\n            percentage = Math.Min(percentage, 100);\n\n            var btn = installationButtons.Find(b => object.ReferenceEquals(b.Tag, cc));\n\n            if (cc.Archived && percentage == 100)\n                btn.Text = \"Unpacking...\".L10N(\"Client:DTAConfig:Unpacking\");\n            else\n                btn.Text = \"Downloading...\".L10N(\"Client:DTAConfig:Downloading\") + \" \" + percentage + \"%\";\n        }\n\n        /// <summary>\n        /// Called whenever a custom component download is finished.\n        /// </summary>\n        /// <param name=\"c\">The CustomComponent object.</param>\n        /// <param name=\"success\">True if the download succeeded, otherwise false.</param>\n        private void cc_DownloadFinished(CustomComponent c, bool success)\n        {\n            WindowManager.AddCallback(new Action<CustomComponent, bool>(HandleDownloadFinished), c, success);\n        }\n\n        private void HandleDownloadFinished(CustomComponent cc, bool success)\n        {\n            cc.DownloadFinished -= cc_DownloadFinished;\n            cc.DownloadProgressChanged -= cc_DownloadProgressChanged;\n\n            var btn = installationButtons.Find(b => object.ReferenceEquals(b.Tag, cc));\n            btn.AllowClick = true;\n\n            if (!success)\n            {\n                if (!downloadCancelled)\n                {\n                    XNAMessageBox.Show(WindowManager, \"Optional Component Download Failed\".L10N(\"Client:DTAConfig:OptionalComponentDownloadFailedTitle\"),\n                        string.Format((\"Download of optional component {0} failed.\\n\" +\n                        \"See client.log for details.\\n\\n\" +\n                        \"If this problem continues, please contact your mod's authors for support.\").L10N(\"Client:DTAConfig:OptionalComponentDownloadFailedText\"),\n                        cc.GUIName));\n                }\n\n                btn.Text = \"Install\".L10N(\"Client:DTAConfig:Install\") + $\" ({GetSizeString(cc.RemoteSize)})\";\n\n                if (SafePath.GetFile(ProgramConstants.GamePath, cc.LocalPath).Exists)\n                    btn.Text = \"Update\".L10N(\"Client:DTAConfig:Update\") + $\" ({GetSizeString(cc.RemoteSize)})\";\n            }\n            else\n            {\n                XNAMessageBox.Show(WindowManager, \"Download Completed\".L10N(\"Client:DTAConfig:DownloadCompleteTitle\"),\n                    string.Format(\"Download of optional component {0} completed succesfully.\".L10N(\"Client:DTAConfig:DownloadCompleteText\"), cc.GUIName));\n                btn.Text = \"Uninstall\".L10N(\"Client:DTAConfig:Uninstall\");\n            }\n        }\n\n        public void CancelAllDownloads()\n        {\n            Logger.Log(\"Cancelling all custom component downloads.\");\n\n            downloadCancelled = true;\n\n            if (Updater.CustomComponents == null)\n                return;\n\n            foreach (CustomComponent cc in Updater.CustomComponents)\n            {\n                if (cc.IsBeingDownloaded)\n                    cc.StopDownload();\n            }\n        }\n\n        public void Open()\n        {\n            downloadCancelled = false;\n        }\n\n        private string GetSizeString(long size)\n        {\n            if (size < 1048576)\n            {\n                return (size / 1024) + \" KB\";\n            }\n            else\n            {\n                return (size / 1048576) + \" MB\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/DisplayOptionsPanel.cs",
    "content": "using ClientCore.Extensions;\nusing ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\n#if WINFORMS\nusing System.Windows.Forms;\n#endif\nusing Microsoft.Win32;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Versioning;\nusing System.IO;\nusing ClientCore.I18N;\nusing ClientCore.Enums;\nusing System.Diagnostics;\nusing System.Linq;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class DisplayOptionsPanel : XNAOptionsPanel\n    {\n        // Mouse must move at least this many pixels from click point before drag selection activates.\n        private const int DRAG_DISTANCE_DEFAULT = 4;\n        private const int ORIGINAL_RESOLUTION_WIDTH = 640;\n\n        private readonly DirectDrawWrapperManager directDrawWrapperManager;\n\n        public DisplayOptionsPanel(WindowManager windowManager, UserINISettings iniSettings, DirectDrawWrapperManager directDrawWrapperManager)\n            : base(windowManager, iniSettings)\n        {\n            this.directDrawWrapperManager = directDrawWrapperManager;\n        }\n\n        private XNAClientDropDown ddIngameResolution;\n        private XNAClientDropDown ddDetailLevel;\n        private XNAClientDropDown ddRenderer;\n        private XNAClientCheckBox chkWindowedMode;\n        private XNAClientCheckBox chkBorderlessWindowedMode;\n        private XNAClientCheckBox chkBackBufferInVRAM;\n        private XNAClientPreferredItemDropDown ddClientResolution;\n        private XNAClientCheckBox chkBorderlessClient;\n        private XNAClientCheckBox chkIntegerScaledClient;\n        private XNAClientDropDown ddClientTheme;\n        private XNAClientDropDown ddTranslation;\n\n        private XNALabel lblCompatibilityFixes;\n        private XNALabel lblGameCompatibilityFix;\n        private XNALabel lblMapEditorCompatibilityFix;\n        private XNAClientButton btnGameCompatibilityFix;\n        private XNAClientButton btnMapEditorCompatibilityFix;\n\n        private bool GameCompatFixInstalled = false;\n        private bool FinalSunCompatFixInstalled = false;\n        private bool GameCompatFixDeclined = false;\n        //private bool FinalSunCompatFixDeclined = false;\n\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"DisplayOptionsPanel\";\n\n            var lblIngameResolution = new XNALabel(WindowManager);\n            lblIngameResolution.Name = nameof(lblIngameResolution);\n            lblIngameResolution.ClientRectangle = new Rectangle(12, 14, 0, 0);\n            lblIngameResolution.Text = \"In-game Resolution:\".L10N(\"Client:DTAConfig:InGameResolution\");\n\n            ddIngameResolution = new XNAClientDropDown(WindowManager);\n            ddIngameResolution.Name = nameof(ddIngameResolution);\n            ddIngameResolution.ClientRectangle = new Rectangle(\n                lblIngameResolution.Right + 12,\n                lblIngameResolution.Y - 2, 120, 19);\n\n            // Add in-game resolutions\n            {\n                var maximumIngameResolution = new ScreenResolution(ClientConfiguration.Instance.MaximumIngameWidth, ClientConfiguration.Instance.MaximumIngameHeight);\n\n#if XNA\n                if (!ScreenResolution.HiDefLimitResolution.Fits(maximumIngameResolution))\n                    maximumIngameResolution = ScreenResolution.HiDefLimitResolution;\n#endif\n\n                SortedSet<ScreenResolution> resolutions = ScreenResolution.GetFullScreenResolutions(\n                    ClientConfiguration.Instance.MinimumIngameWidth, ClientConfiguration.Instance.MinimumIngameHeight,\n                    maximumIngameResolution.Width, maximumIngameResolution.Height);\n\n                foreach (var res in resolutions)\n                    ddIngameResolution.AddItem(res.ToString());\n            }\n\n            var lblDetailLevel = new XNALabel(WindowManager);\n            lblDetailLevel.Name = nameof(lblDetailLevel);\n            lblDetailLevel.ClientRectangle = new Rectangle(lblIngameResolution.X,\n                ddIngameResolution.Bottom + 16, 0, 0);\n            lblDetailLevel.Text = \"Detail Level:\".L10N(\"Client:DTAConfig:DetailLevel\");\n\n            ddDetailLevel = new XNAClientDropDown(WindowManager);\n            ddDetailLevel.Name = nameof(ddDetailLevel);\n            ddDetailLevel.ClientRectangle = new Rectangle(\n                ddIngameResolution.X,\n                lblDetailLevel.Y - 2,\n                ddIngameResolution.Width,\n                ddIngameResolution.Height);\n            ddDetailLevel.AddItem(\"Low\".L10N(\"Client:DTAConfig:DetailLevelLow\"));\n            ddDetailLevel.AddItem(\"Medium\".L10N(\"Client:DTAConfig:DetailLevelMedium\"));\n            ddDetailLevel.AddItem(\"High\".L10N(\"Client:DTAConfig:DetailLevelHigh\"));\n\n            var lblRenderer = new XNALabel(WindowManager);\n            lblRenderer.Name = nameof(lblRenderer);\n            lblRenderer.ClientRectangle = new Rectangle(lblDetailLevel.X,\n                ddDetailLevel.Bottom + 16, 0, 0);\n            lblRenderer.Text = \"Renderer:\".L10N(\"Client:DTAConfig:Renderer\");\n\n            ddRenderer = new XNAClientDropDown(WindowManager);\n            ddRenderer.Name = nameof(ddRenderer);\n            ddRenderer.ClientRectangle = new Rectangle(\n                ddDetailLevel.X,\n                lblRenderer.Y - 2,\n                ddDetailLevel.Width,\n                ddDetailLevel.Height);\n\n            foreach (var renderer in directDrawWrapperManager.GetRenderers(ClientConfiguration.Instance.GetOperatingSystemVersion()))\n            {\n                ddRenderer.AddItem(new XNADropDownItem()\n                {\n                    Text = renderer.UIName,\n                    Tag = renderer\n                });\n            }\n\n            chkWindowedMode = new XNAClientCheckBox(WindowManager);\n            chkWindowedMode.Name = nameof(chkWindowedMode);\n            chkWindowedMode.ClientRectangle = new Rectangle(lblDetailLevel.X,\n                ddRenderer.Bottom + 16, 0, 0);\n            chkWindowedMode.Text = \"Windowed Mode\".L10N(\"Client:DTAConfig:WindowedMode\");\n            chkWindowedMode.CheckedChanged += ChkWindowedMode_CheckedChanged;\n\n            chkBorderlessWindowedMode = new XNAClientCheckBox(WindowManager);\n            chkBorderlessWindowedMode.Name = nameof(chkBorderlessWindowedMode);\n            chkBorderlessWindowedMode.ClientRectangle = new Rectangle(\n                chkWindowedMode.X + 50,\n                chkWindowedMode.Bottom + 24, 0, 0);\n            chkBorderlessWindowedMode.Text = \"Borderless Windowed Mode\".L10N(\"Client:DTAConfig:BorderlessWindowedMode\");\n            chkBorderlessWindowedMode.AllowChecking = false;\n\n            chkBackBufferInVRAM = new XNAClientCheckBox(WindowManager);\n            chkBackBufferInVRAM.Name = nameof(chkBackBufferInVRAM);\n            chkBackBufferInVRAM.ClientRectangle = new Rectangle(\n                lblDetailLevel.X,\n                chkBorderlessWindowedMode.Bottom + 28, 0, 0);\n            chkBackBufferInVRAM.Text = (\"Back Buffer in Video Memory\\n(lower performance, but is\\nnecessary on some systems)\").L10N(\"Client:DTAConfig:BackBuffer\");\n\n            var lblClientResolution = new XNALabel(WindowManager);\n            lblClientResolution.Name = nameof(lblClientResolution);\n            lblClientResolution.ClientRectangle = new Rectangle(\n                285, 14, 0, 0);\n            lblClientResolution.Text = \"Client Resolution:\".L10N(\"Client:DTAConfig:ClientResolution\");\n\n            ddClientResolution = new XNAClientPreferredItemDropDown(WindowManager);\n            ddClientResolution.Name = nameof(ddClientResolution);\n            ddClientResolution.ClientRectangle = new Rectangle(\n                lblClientResolution.Right + 12,\n                lblClientResolution.Y - 2,\n                Width - (lblClientResolution.Right + 24),\n                ddIngameResolution.Height);\n            ddClientResolution.AllowDropDown = false;\n            ddClientResolution.PreferredItemLabel = \"(recommended)\".L10N(\"Client:DTAConfig:Recommended\");\n\n            // Add client resolutions\n            {\n                SortedSet<ScreenResolution> scaledRecommendedResolutions = ScreenResolution.GetRecommendedResolutions();\n\n                SortedSet<ScreenResolution> resolutions = [\n                    .. ScreenResolution.GetFullScreenResolutions(minWidth: 800, minHeight: 600),\n                    .. ScreenResolution.GetWindowedResolutions(minWidth: 800, minHeight: 600),\n                    .. scaledRecommendedResolutions,\n                ];\n                List<ScreenResolution> resolutionList = resolutions.ToList();\n\n                foreach (ScreenResolution res in resolutionList)\n                {\n                    var item = new XNADropDownItem();\n                    item.Text = res.ToString();\n                    item.Tag = res.ToString();\n                    ddClientResolution.AddItem(item);\n                }\n\n                // So we add the optimal resolutions to the list, sort it and then find\n                // out the optimal resolution index - it's inefficient, but works\n                // Note: ddClientResolution.PreferredItemIndexes is assumed in ascending order\n\n                foreach (ScreenResolution scaledRecommendedResolution in scaledRecommendedResolutions)\n                {\n                    int index = resolutionList.FindIndex(res => res == scaledRecommendedResolution);\n                    if (index > -1)\n                        ddClientResolution.PreferredItemIndexes.Add(index);\n                }\n            }\n\n            chkBorderlessClient = new XNAClientCheckBox(WindowManager);\n            chkBorderlessClient.Name = nameof(chkBorderlessClient);\n            chkBorderlessClient.ClientRectangle = new Rectangle(\n                lblClientResolution.X,\n                lblDetailLevel.Y, 0, 0);\n            chkBorderlessClient.Text = \"Fullscreen Client\".L10N(\"Client:DTAConfig:FullscreenClient\");\n            chkBorderlessClient.CheckedChanged += ChkBorderlessMenu_CheckedChanged;\n            chkBorderlessClient.Checked = true;\n\n            chkIntegerScaledClient = new XNAClientCheckBox(WindowManager);\n            chkIntegerScaledClient.Name = nameof(chkIntegerScaledClient);\n            chkIntegerScaledClient.ClientRectangle = new Rectangle(\n                lblClientResolution.X,\n                lblRenderer.Y, 0, 0);\n            chkIntegerScaledClient.Text = \"Integer Scaled Client\".L10N(\"Client:DTAConfig:IntegerScaledClient\");\n            chkIntegerScaledClient.Checked = IniSettings.IntegerScaledClient.Value;\n            chkIntegerScaledClient.ToolTipText =\n                \"\"\"\n                Enable integer scaling for the client. This will cause the client to use\n                the closest fitting resolution that is required to maintain sharp graphics,\n                at the expense of black borders that may appear at some resolutions.\n\n                Additionally, enabling this option will also allow the client window \n                to be resized (does not affect the selected client resolution).\n                \"\"\"\n                .L10N(\"Client:DTAConfig:IntegerScaledClientToolTip\");\n\n            var lblClientTheme = new XNALabel(WindowManager);\n            lblClientTheme.Name = nameof(lblClientTheme);\n            lblClientTheme.ClientRectangle = new Rectangle(\n                lblClientResolution.X,\n                chkWindowedMode.Y, 0, 0);\n            lblClientTheme.Text = \"Client Theme:\".L10N(\"Client:DTAConfig:ClientTheme\");\n\n            ddClientTheme = new XNAClientDropDown(WindowManager);\n            ddClientTheme.Name = nameof(ddClientTheme);\n            ddClientTheme.ClientRectangle = new Rectangle(\n                ddClientResolution.X,\n                chkWindowedMode.Y,\n                ddClientResolution.Width,\n                ddRenderer.Height);\n\n            int themeCount = ClientConfiguration.Instance.ThemeCount;\n\n            for (int i = 0; i < themeCount; i++)\n            {\n                string themeName = ClientConfiguration.Instance.GetThemeInfoFromIndex(i).Name;\n\n                string displayName = themeName.L10N($\"INI:Themes:{themeName}\");\n                ddClientTheme.AddItem(new XNADropDownItem { Text = displayName, Tag = themeName });\n            }\n\n            var lblTranslation = new XNALabel(WindowManager);\n            lblTranslation.Name = nameof(lblTranslation);\n            lblTranslation.ClientRectangle = new Rectangle(\n                lblClientTheme.X,\n                ddClientTheme.Bottom + 16, 0, 0);\n            lblTranslation.Text = \"Language:\".L10N(\"Client:DTAConfig:Language\");\n\n            ddTranslation = new XNAClientDropDown(WindowManager);\n            ddTranslation.Name = nameof(ddTranslation);\n            ddTranslation.ClientRectangle = new Rectangle(\n                ddClientTheme.X,\n                lblTranslation.Y - 2,\n                ddClientTheme.Width,\n                ddClientTheme.Height);\n\n            foreach (var (translation, name) in Translation.GetTranslations())\n                ddTranslation.AddItem(new XNADropDownItem { Text = name, Tag = translation });\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                AddCompatibilityFixControls();\n            }\n\n            AddChild(chkWindowedMode);\n            AddChild(chkBorderlessWindowedMode);\n            AddChild(chkBackBufferInVRAM);\n            AddChild(chkBorderlessClient);\n            AddChild(chkIntegerScaledClient);\n            AddChild(lblClientTheme);\n            AddChild(ddClientTheme);\n            AddChild(lblTranslation);\n            AddChild(ddTranslation);\n            AddChild(lblClientResolution);\n            AddChild(ddClientResolution);\n            AddChild(lblRenderer);\n            AddChild(ddRenderer);\n            AddChild(lblDetailLevel);\n            AddChild(ddDetailLevel);\n            AddChild(lblIngameResolution);\n            AddChild(ddIngameResolution);\n        }\n\n        [SupportedOSPlatform(\"windows\")]\n        private void AddCompatibilityFixControls()\n        {\n            lblCompatibilityFixes = new XNALabel(WindowManager);\n            lblCompatibilityFixes.Name = \"lblCompatibilityFixes\";\n            lblCompatibilityFixes.FontIndex = 1;\n            lblCompatibilityFixes.Text = \"Legacy Compatibility Fixes:\".L10N(\"Client:DTAConfig:TSCompatibilityFixLegacy\");\n            AddChild(lblCompatibilityFixes);\n            lblCompatibilityFixes.CenterOnParent();\n            lblCompatibilityFixes.Y = Height - 97;\n\n            lblGameCompatibilityFix = new XNALabel(WindowManager);\n            lblGameCompatibilityFix.Name = \"lblGameCompatibilityFix\";\n            lblGameCompatibilityFix.ClientRectangle = new Rectangle(132,\n                lblCompatibilityFixes.Bottom + 20, 0, 0);\n            lblGameCompatibilityFix.Text = \"DTA/TI/TS Compatibility Fix:\".L10N(\"Client:DTAConfig:TSCompatibilityFix\");\n\n            btnGameCompatibilityFix = new XNAClientButton(WindowManager);\n            btnGameCompatibilityFix.Name = \"btnGameCompatibilityFix\";\n            btnGameCompatibilityFix.ClientRectangle = new Rectangle(\n                lblGameCompatibilityFix.Right + 20,\n                lblGameCompatibilityFix.Y - 4, 133, 23);\n            btnGameCompatibilityFix.FontIndex = 1;\n            btnGameCompatibilityFix.Text = \"Disable\".L10N(\"Client:DTAConfig:TSDisable\");\n            btnGameCompatibilityFix.LeftClick += BtnGameCompatibilityFix_LeftClick;\n\n            lblMapEditorCompatibilityFix = new XNALabel(WindowManager);\n            lblMapEditorCompatibilityFix.Name = \"lblMapEditorCompatibilityFix\";\n            lblMapEditorCompatibilityFix.ClientRectangle = new Rectangle(\n                lblGameCompatibilityFix.X,\n                lblGameCompatibilityFix.Bottom + 20, 0, 0);\n            lblMapEditorCompatibilityFix.Text = \"FinalSun Compatibility Fix:\".L10N(\"Client:DTAConfig:TSFinalSunFix\");\n\n            btnMapEditorCompatibilityFix = new XNAClientButton(WindowManager);\n            btnMapEditorCompatibilityFix.Name = \"btnMapEditorCompatibilityFix\";\n            btnMapEditorCompatibilityFix.ClientRectangle = new Rectangle(\n                btnGameCompatibilityFix.X,\n                lblMapEditorCompatibilityFix.Y - 4,\n                btnGameCompatibilityFix.Width,\n                btnGameCompatibilityFix.Height);\n            btnMapEditorCompatibilityFix.FontIndex = 1;\n            btnMapEditorCompatibilityFix.Text = \"Disable\".L10N(\"Client:DTAConfig:TSDisable\");\n            btnMapEditorCompatibilityFix.LeftClick += BtnMapEditorCompatibilityFix_LeftClick;\n\n            AddChild(lblGameCompatibilityFix);\n            AddChild(btnGameCompatibilityFix);\n            AddChild(lblMapEditorCompatibilityFix);\n            AddChild(btnMapEditorCompatibilityFix);\n\n            RegistryKey regKey = Registry.CurrentUser.OpenSubKey(\"SOFTWARE\\\\Tiberian Sun Client\");\n\n            if (regKey == null)\n                return;\n\n            object tsCompatFixValue = regKey.GetValue(\"TSCompatFixInstalled\", \"No\");\n            string tsCompatFixString = (string)tsCompatFixValue;\n\n            if (tsCompatFixString == \"Yes\")\n            {\n                GameCompatFixInstalled = true;\n            }\n\n            object fsCompatFixValue = regKey.GetValue(\"FSCompatFixInstalled\", \"No\");\n            string fsCompatFixString = (string)fsCompatFixValue;\n\n            if (fsCompatFixString == \"Yes\")\n            {\n                FinalSunCompatFixInstalled = true;\n            }\n\n            // These compatibility fixes from 2015 are no longer necessary on modern systems.\n            // They are only offered for uninstallation; if they are not installed, hide them.\n            if (!FinalSunCompatFixInstalled)\n            {\n                lblMapEditorCompatibilityFix.Disable();\n                btnMapEditorCompatibilityFix.Disable();\n            }\n\n            if (!GameCompatFixInstalled)\n            {\n                lblGameCompatibilityFix.Disable();\n                btnGameCompatibilityFix.Disable();\n            }\n\n            if (!FinalSunCompatFixInstalled && !GameCompatFixInstalled)\n            {\n                lblCompatibilityFixes.Disable();\n            }\n        }\n\n        /// <summary>\n        /// Asks the user whether they want to install the DTA/TI/TS compatibility fix.\n        /// </summary>\n        public void PostInit()\n        {\n            Load();\n        }\n\n        [SupportedOSPlatform(\"windows\")]\n        private void BtnGameCompatibilityFix_LeftClick(object sender, EventArgs e)\n        {\n            if (GameCompatFixInstalled)\n            {\n                try\n                {\n                    Process sdbinst = Process.Start(\"sdbinst.exe\", \"-q -n \\\"TS Compatibility Fix\\\"\");\n\n                    sdbinst.WaitForExit();\n\n                    Logger.Log(\"DTA/TI/TS Compatibility Fix succesfully uninstalled.\");\n                    XNAMessageBox.Show(WindowManager, \"Compatibility Fix Uninstalled\".L10N(\"Client:DTAConfig:TSFixUninstallTitle\"),\n                        \"The DTA/TI/TS Compatibility Fix has been succesfully uninstalled.\".L10N(\"Client:DTAConfig:TSFixUninstallText\"));\n\n                    RegistryKey regKey = Registry.CurrentUser.OpenSubKey(\"SOFTWARE\", true);\n                    regKey = regKey.CreateSubKey(\"Tiberian Sun Client\");\n                    regKey.SetValue(\"TSCompatFixInstalled\", \"No\");\n\n                    GameCompatFixInstalled = false;\n\n                    lblGameCompatibilityFix.Disable();\n                    btnGameCompatibilityFix.Disable();\n\n                    if (!FinalSunCompatFixInstalled)\n                        lblCompatibilityFixes.Disable();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Uninstalling DTA/TI/TS Compatibility Fix failed. Error message: \" + ex.ToString());\n                    XNAMessageBox.Show(WindowManager, \"Uninstalling Compatibility Fix Failed\".L10N(\"Client:DTAConfig:TSFixUninstallFailTitle\"),\n                        \"Uninstalling DTA/TI/TS Compatibility Fix failed. Returned error:\".L10N(\"Client:DTAConfig:TSFixUninstallFailText\") + \" \" + ex.Message);\n                }\n\n                return;\n            }\n        }\n\n        [SupportedOSPlatform(\"windows\")]\n        private void BtnMapEditorCompatibilityFix_LeftClick(object sender, EventArgs e)\n        {\n            if (FinalSunCompatFixInstalled)\n            {\n                try\n                {\n                    Process sdbinst = Process.Start(\"sdbinst.exe\", \"-q -n \\\"Final Sun Compatibility Fix\\\"\");\n\n                    sdbinst.WaitForExit();\n\n                    RegistryKey regKey = Registry.CurrentUser.OpenSubKey(\"SOFTWARE\", true);\n                    regKey = regKey.CreateSubKey(\"Tiberian Sun Client\");\n                    regKey.SetValue(\"FSCompatFixInstalled\", \"No\");\n\n                    btnMapEditorCompatibilityFix.Text = \"Enable\".L10N(\"Client:DTAConfig:TSButtonEnable\");\n\n                    Logger.Log(\"FinalSun Compatibility Fix succesfully uninstalled.\");\n                    XNAMessageBox.Show(WindowManager, \"Compatibility Fix Uninstalled\".L10N(\"Client:DTAConfig:TSFinalSunFixUninstallTitle\"),\n                        \"The FinalSun Compatibility Fix has been succesfully uninstalled.\".L10N(\"Client:DTAConfig:TSFinalSunFixUninstallText\"));\n\n                    FinalSunCompatFixInstalled = false;\n\n                    lblMapEditorCompatibilityFix.Disable();\n                    btnMapEditorCompatibilityFix.Disable();\n\n                    if (!GameCompatFixInstalled)\n                        lblCompatibilityFixes.Disable();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Uninstalling FinalSun Compatibility Fix failed. Error message: \" + ex.ToString());\n                    XNAMessageBox.Show(WindowManager, \"Uninstalling Compatibility Fix Failed\".L10N(\"Client:DTAConfig:TSFinalSunFixUninstallFailedTitle\"),\n                        \"Uninstalling FinalSun Compatibility Fix failed. Error message:\".L10N(\"Client:DTAConfig:TSFinalSunFixUninstallFailedText\") + \" \" + ex.Message);\n                }\n\n                return;\n            }\n        }\n\n        private void ChkBorderlessMenu_CheckedChanged(object sender, EventArgs e)\n        {\n            if (chkBorderlessClient.Checked)\n            {\n                ddClientResolution.AllowDropDown = false;\n\n                string nativeRes = ScreenResolution.SafeFullScreenResolution;\n\n                int nativeResIndex = ddClientResolution.Items.FindIndex(i => (string)i.Tag == nativeRes);\n                if (nativeResIndex > -1)\n                    ddClientResolution.SelectedIndex = nativeResIndex;\n            }\n            else\n            {\n                ddClientResolution.AllowDropDown = true;\n\n                if (ddClientResolution.PreferredItemIndexes.Count > 0)\n                {\n                    // Note: ddClientResolution.PreferredItemIndexes is assumed in ascending order\n                    int optimalWindowedResIndex = ddClientResolution.PreferredItemIndexes[^1];\n                    ddClientResolution.SelectedIndex = optimalWindowedResIndex;\n                }\n            }\n        }\n\n        private void ChkWindowedMode_CheckedChanged(object sender, EventArgs e)\n        {\n            if (chkWindowedMode.Checked)\n            {\n                chkBorderlessWindowedMode.AllowChecking = true;\n                return;\n            }\n\n            chkBorderlessWindowedMode.AllowChecking = false;\n            chkBorderlessWindowedMode.Checked = false;\n        }\n\n        /// <summary>\n        /// Loads the user's preferred renderer.\n        /// </summary>\n        private void LoadRenderer()\n        {\n            int index = ddRenderer.Items.FindIndex(\n                           r => ((DirectDrawWrapper)r.Tag).InternalName == directDrawWrapperManager.SelectedRenderer.InternalName);\n\n            if (index < 0 && directDrawWrapperManager.SelectedRenderer.Hidden)\n            {\n                ddRenderer.AddItem(new XNADropDownItem()\n                {\n                    Text = directDrawWrapperManager.SelectedRenderer.UIName,\n                    Tag = directDrawWrapperManager.SelectedRenderer\n                });\n                index = ddRenderer.Items.Count - 1;\n            }\n\n            ddRenderer.SelectedIndex = index;\n        }\n\n        public override void Load()\n        {\n            base.Load();\n\n            LoadRenderer();\n            ddDetailLevel.SelectedIndex = UserINISettings.Instance.DetailLevel;\n\n            string currentRes = UserINISettings.Instance.IngameScreenWidth.Value +\n                \"x\" + UserINISettings.Instance.IngameScreenHeight.Value;\n\n            int index = ddIngameResolution.Items.FindIndex(i => i.Text == currentRes);\n\n            ddIngameResolution.SelectedIndex = index > -1 ? index : 0;\n\n            // Wonder what this \"Win8CompatMode\" actually does..\n            // Disabling it used to be TS-DDRAW only, but it was never enabled after \n            // you had tried TS-DDRAW once, so most players probably have it always\n            // disabled anyway\n            IniSettings.Win8CompatMode.Value = \"No\";\n\n            var renderer = (DirectDrawWrapper)ddRenderer.SelectedItem.Tag;\n\n            if (renderer.UsesCustomWindowedOption())\n            {\n                // For renderers that have their own windowed mode implementation\n                // enabled through their own config INI file\n                // (for example DxWnd and CnC-DDRAW)\n\n                IniFile rendererSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, renderer.ConfigFileName));\n\n                chkWindowedMode.Checked = rendererSettingsIni.GetBooleanValue(renderer.WindowedModeSection,\n                    renderer.WindowedModeKey, false);\n\n                if (!string.IsNullOrEmpty(renderer.BorderlessWindowedModeKey))\n                {\n                    bool setting = rendererSettingsIni.GetBooleanValue(renderer.WindowedModeSection,\n                        renderer.BorderlessWindowedModeKey, false);\n                    chkBorderlessWindowedMode.Checked = renderer.IsBorderlessWindowedModeKeyReversed ? !setting : setting;\n                }\n                else\n                {\n                    chkBorderlessWindowedMode.Checked = UserINISettings.Instance.BorderlessWindowedMode;\n                }\n            }\n            else\n            {\n                chkWindowedMode.Checked = UserINISettings.Instance.WindowedMode;\n                chkBorderlessWindowedMode.Checked = UserINISettings.Instance.BorderlessWindowedMode;\n            }\n\n            string currentClientRes = IniSettings.ClientResolutionX.Value + \"x\" + IniSettings.ClientResolutionY.Value;\n\n            int clientResIndex = ddClientResolution.Items.FindIndex(i => (string)i.Tag == currentClientRes);\n\n            ddClientResolution.SelectedIndex = clientResIndex > -1 ? clientResIndex : 0;\n\n            chkBorderlessClient.Checked = UserINISettings.Instance.BorderlessWindowedClient;\n\n            int selectedThemeIndex = ddClientTheme.Items.FindIndex(\n                ddi => (string)ddi.Tag == UserINISettings.Instance.ClientTheme);\n            ddClientTheme.SelectedIndex = selectedThemeIndex > -1 ? selectedThemeIndex : 0;\n\n            foreach (string localeCode in new string[] { UserINISettings.Instance.Translation, Translation.GetDefaultTranslationLocaleCode(), ProgramConstants.HARDCODED_LOCALE_CODE })\n            {\n                int selectedTranslationIndex = ddTranslation.Items.FindIndex(\n                    ddi => localeCode.Equals((string)ddi.Tag, StringComparison.InvariantCultureIgnoreCase));\n\n                if (selectedTranslationIndex > -1)\n                {\n                    ddTranslation.SelectedIndex = selectedTranslationIndex;\n                    break;\n                }\n            }\n\n            Debug.Assert(ddTranslation.SelectedIndex > -1, \"No translation was selected\");\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n            {\n                chkBackBufferInVRAM.Checked = !UserINISettings.Instance.BackBufferInVRAM;\n            }\n            else\n            {\n                chkBackBufferInVRAM.Checked = UserINISettings.Instance.BackBufferInVRAM;\n            }\n        }\n\n        public override bool Save()\n        {\n            bool restartRequired = base.Save();\n\n            IniSettings.DetailLevel.Value = ddDetailLevel.SelectedIndex;\n\n            ScreenResolution ingameRes = ddIngameResolution.SelectedItem.Text;\n\n            (IniSettings.IngameScreenWidth.Value, IniSettings.IngameScreenHeight.Value) = ingameRes;\n\n            // Calculate drag selection distance, scale it with resolution width\n            // CustomDragDistance > 0 overrides auto-scaling for players who need a specific value\n            int dragDistance = IniSettings.CustomDragDistance.Value > 0\n                ? IniSettings.CustomDragDistance.Value\n                : ingameRes.Width / ORIGINAL_RESOLUTION_WIDTH * DRAG_DISTANCE_DEFAULT;\n            IniSettings.DragDistance.Value = dragDistance;\n\n            var newSelectedRenderer = (DirectDrawWrapper)ddRenderer.SelectedItem.Tag;\n            bool isChangingRenderer = newSelectedRenderer != directDrawWrapperManager.SelectedRenderer;\n\n            IniSettings.WindowedMode.Value = chkWindowedMode.Checked &&\n                !newSelectedRenderer.UsesCustomWindowedOption();\n\n            IniSettings.BorderlessWindowedMode.Value = chkBorderlessWindowedMode.Checked &&\n                string.IsNullOrEmpty(newSelectedRenderer.BorderlessWindowedModeKey);\n\n            ScreenResolution clientRes = (string)ddClientResolution.SelectedItem.Tag;\n\n            if (clientRes.Width != IniSettings.ClientResolutionX.Value ||\n                clientRes.Height != IniSettings.ClientResolutionY.Value)\n                restartRequired = true;\n\n            // 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.\n\n            (IniSettings.ClientResolutionX.Value, IniSettings.ClientResolutionY.Value) = clientRes;\n\n            if (IniSettings.BorderlessWindowedClient.Value != chkBorderlessClient.Checked)\n                restartRequired = true;\n\n            IniSettings.BorderlessWindowedClient.Value = chkBorderlessClient.Checked;\n\n            if (IniSettings.IntegerScaledClient.Value != chkIntegerScaledClient.Checked)\n                restartRequired = true;\n\n            IniSettings.IntegerScaledClient.Value = chkIntegerScaledClient.Checked;\n\n            restartRequired = restartRequired || IniSettings.ClientTheme != (string)ddClientTheme.SelectedItem.Tag;\n\n            IniSettings.ClientTheme.Value = (string)ddClientTheme.SelectedItem.Tag;\n\n            {\n                bool updateTranslation = !IniSettings.Translation.ToString().Equals((string)ddTranslation.SelectedItem.Tag, StringComparison.InvariantCultureIgnoreCase);\n\n                restartRequired = restartRequired || updateTranslation;\n\n                IniSettings.Translation.Value = (string)ddTranslation.SelectedItem.Tag;\n\n                if (updateTranslation)\n                    IniSettings.TranslationGameFilesVersion.Value = string.Empty;\n            }\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n                IniSettings.BackBufferInVRAM.Value = !chkBackBufferInVRAM.Checked;\n            else\n                IniSettings.BackBufferInVRAM.Value = chkBackBufferInVRAM.Checked;\n\n            directDrawWrapperManager.Save(newSelectedRenderer);\n\n            if (directDrawWrapperManager.SelectedRenderer.UsesCustomWindowedOption())\n            {\n                IniFile rendererSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, directDrawWrapperManager.SelectedRenderer.ConfigFileName));\n\n                rendererSettingsIni.SetBooleanValue(directDrawWrapperManager.SelectedRenderer.WindowedModeSection,\n                    directDrawWrapperManager.SelectedRenderer.WindowedModeKey, chkWindowedMode.Checked);\n\n                if (!string.IsNullOrEmpty(directDrawWrapperManager.SelectedRenderer.BorderlessWindowedModeKey))\n                {\n                    bool borderlessModeIniValue = chkBorderlessWindowedMode.Checked;\n                    if (directDrawWrapperManager.SelectedRenderer.IsBorderlessWindowedModeKeyReversed)\n                        borderlessModeIniValue = !borderlessModeIniValue;\n\n                    rendererSettingsIni.SetBooleanValue(directDrawWrapperManager.SelectedRenderer.WindowedModeSection,\n                        directDrawWrapperManager.SelectedRenderer.BorderlessWindowedModeKey, borderlessModeIniValue);\n                }\n\n                rendererSettingsIni.WriteIniFile();\n            }\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n            {\n                if (ClientConfiguration.Instance.CopyResolutionDependentLanguageDLL)\n                {\n                    string languageDllDestinationPath = SafePath.CombineFilePath(ProgramConstants.GamePath, \"Language.dll\");\n\n                    FileInfo fileInfo = SafePath.GetFile(languageDllDestinationPath);\n                    if (fileInfo.Exists)\n                    {\n                        fileInfo.IsReadOnly = false;\n                        fileInfo.Delete();\n                    }\n\n                    if (ingameRes.Width >= 1024 && ingameRes.Height >= 720)\n                        File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Resources\", \"language_1024x720.dll\"), languageDllDestinationPath);\n                    else if (ingameRes.Width >= 800 && ingameRes.Height >= 600)\n                        File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Resources\", \"language_800x600.dll\"), languageDllDestinationPath);\n                    else\n                        File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Resources\", \"language_640x480.dll\"), languageDllDestinationPath);\n                }\n            }\n\n#if ISWINDOWS\n            // Since `CheckAndPromptFix` method might restart the client if the admin rights are required, we do this at the end of the Save() method\n            if (isChangingRenderer && !directDrawWrapperManager.SelectedRenderer.IsDummy)\n                DirectDrawCompatibilityChecker.CheckAndPromptFix(WindowManager);\n#endif\n\n            return restartRequired;\n        }\n\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/GameOptionsPanel.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing ClientCore.Enums;\nusing ClientGUI.Settings;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class GameOptionsPanel : XNAOptionsPanel\n    {\n\n        private const string TEXT_BACKGROUND_COLOR_TRANSPARENT = \"0\";\n        private const string TEXT_BACKGROUND_COLOR_BLACK = \"12\";\n        private const int MAX_SCROLL_RATE = 6;\n\n        public GameOptionsPanel(WindowManager windowManager, UserINISettings iniSettings, XNAControl topBar)\n            : base(windowManager, iniSettings)\n        {\n            this.topBar = topBar;\n        }\n\n        private XNALabel lblScrollRateValue;\n\n        private XNATrackbar trbScrollRate;\n        private XNAClientCheckBox chkTargetLines;\n        private XNAClientCheckBox chkScrollCoasting;\n        private XNAClientCheckBox chkTooltips;\n        private XNAClientCheckBox chkAltToUndeploy;\n        private XNAClientCheckBox chkBlackChatBackground;\n        private XNAClientCheckBox chkShowHiddenObjects;\n\n        private XNAControl topBar;\n\n        private XNATextBox tbPlayerName;\n\n        private HotkeyConfigurationWindow hotkeyConfigWindow;\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"GameOptionsPanel\";\n\n            var lblScrollRate = new XNALabel(WindowManager);\n            lblScrollRate.Name = nameof(lblScrollRate);\n            lblScrollRate.ClientRectangle = new Rectangle(12,\n                14, 0, 0);\n            lblScrollRate.Text = \"Scroll Rate:\".L10N(\"Client:DTAConfig:ScrollRate\");\n\n            lblScrollRateValue = new XNALabel(WindowManager);\n            lblScrollRateValue.Name = nameof(lblScrollRateValue);\n            lblScrollRateValue.FontIndex = 1;\n            lblScrollRateValue.Text = \"0\";\n            lblScrollRateValue.ClientRectangle = new Rectangle(\n                Width - lblScrollRateValue.Width - 12,\n                lblScrollRate.Y, 0, 0);\n\n            trbScrollRate = new XNATrackbar(WindowManager);\n            trbScrollRate.Name = nameof(trbScrollRate);\n            trbScrollRate.ClientRectangle = new Rectangle(\n                lblScrollRate.Right + 32,\n                lblScrollRate.Y - 2,\n                lblScrollRateValue.X - lblScrollRate.Right - 47,\n                22);\n            trbScrollRate.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2);\n            trbScrollRate.MinValue = 0;\n            trbScrollRate.MaxValue = MAX_SCROLL_RATE;\n            trbScrollRate.ValueChanged += TrbScrollRate_ValueChanged;\n\n            chkScrollCoasting = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, \"ScrollMethod\", true, \"0\", \"1\");\n            chkScrollCoasting.Name = nameof(chkScrollCoasting);\n            chkScrollCoasting.ClientRectangle = new Rectangle(\n                lblScrollRate.X,\n                trbScrollRate.Bottom + 20, 0, 0);\n            chkScrollCoasting.Text = \"Scroll Coasting\".L10N(\"Client:DTAConfig:ScrollCoasting\");\n\n            chkTargetLines = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, \"UnitActionLines\");\n            chkTargetLines.Name = nameof(chkTargetLines);\n            chkTargetLines.ClientRectangle = new Rectangle(\n                lblScrollRate.X,\n                chkScrollCoasting.Bottom + 24, 0, 0);\n            chkTargetLines.Text = \"Target Lines\".L10N(\"Client:DTAConfig:TargetLines\");\n\n            chkTooltips = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, \"ToolTips\");\n            chkTooltips.Name = nameof(chkTooltips);\n            chkTooltips.Text = \"Tooltips\".L10N(\"Client:DTAConfig:Tooltips\");\n\n            var lblPlayerName = new XNALabel(WindowManager);\n            lblPlayerName.Name = nameof(lblPlayerName);\n            lblPlayerName.Text = \"Player Name*:\".L10N(\"Client:DTAConfig:PlayerName\");\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n            {\n                chkTooltips.ClientRectangle = new Rectangle(\n                    lblScrollRate.X,\n                    chkTargetLines.Bottom + 24, 0, 0);\n            }\n            else\n            {\n                chkShowHiddenObjects = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, \"ShowHidden\");\n                chkShowHiddenObjects.Name = nameof(chkShowHiddenObjects);\n                chkShowHiddenObjects.ClientRectangle = new Rectangle(\n                    lblScrollRate.X,\n                    chkTargetLines.Bottom + 24, 0, 0);\n                chkShowHiddenObjects.Text = \"Show Hidden Objects\".L10N(\"Client:DTAConfig:YRShowHidden\");\n\n                chkTooltips.ClientRectangle = new Rectangle(\n                    lblScrollRate.X,\n                    chkShowHiddenObjects.Bottom + 24, 0, 0);\n\n                lblPlayerName.ClientRectangle = new Rectangle(\n                    lblScrollRate.X,\n                    chkTooltips.Bottom + 30, 0, 0);\n\n                AddChild(chkShowHiddenObjects);\n            }\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n            {\n                chkBlackChatBackground = new SettingCheckBox(WindowManager, false, UserINISettings.OPTIONS, \"TextBackgroundColor\", true, TEXT_BACKGROUND_COLOR_BLACK, TEXT_BACKGROUND_COLOR_TRANSPARENT);\n                chkBlackChatBackground.Name = nameof(chkBlackChatBackground);\n                chkBlackChatBackground.ClientRectangle = new Rectangle(\n                    chkScrollCoasting.X,\n                    chkTooltips.Bottom + 24, 0, 0);\n                chkBlackChatBackground.Text = \"Use black background for in-game chat messages\".L10N(\"Client:DTAConfig:TSUseBlackBackgroundChat\");\n\n                AddChild(chkBlackChatBackground);\n\n                chkAltToUndeploy = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, \"MoveToUndeploy\");\n                chkAltToUndeploy.Name = nameof(chkAltToUndeploy);\n                chkAltToUndeploy.ClientRectangle = new Rectangle(\n                    chkScrollCoasting.X,\n                    chkBlackChatBackground.Bottom + 24, 0, 0);\n                chkAltToUndeploy.Text = \"Undeploy units by holding Alt key instead of a regular move command\".L10N(\"Client:DTAConfig:TSUndeployAltKey\");\n\n                AddChild(chkAltToUndeploy);\n\n                lblPlayerName.ClientRectangle = new Rectangle(\n                    lblScrollRate.X,\n                    chkAltToUndeploy.Bottom + 30, 0, 0);\n            }\n\n            tbPlayerName = new XNATextBox(WindowManager);\n            tbPlayerName.Name = nameof(tbPlayerName);\n            tbPlayerName.MaximumTextLength = ClientConfiguration.Instance.MaxNameLength;\n            tbPlayerName.ClientRectangle = new Rectangle(trbScrollRate.X,\n                lblPlayerName.Y - 2, 200, 19);\n            tbPlayerName.Text = ProgramConstants.PLAYERNAME;\n\n            var lblNotice = new XNALabel(WindowManager);\n            lblNotice.Name = nameof(lblNotice);\n            lblNotice.ClientRectangle = new Rectangle(lblPlayerName.X,\n                lblPlayerName.Bottom + 30, 0, 0);\n            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\");\n\n            hotkeyConfigWindow = new HotkeyConfigurationWindow(WindowManager);\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, hotkeyConfigWindow);\n            hotkeyConfigWindow.Disable();\n\n            var btnConfigureHotkeys = new XNAClientButton(WindowManager);\n            btnConfigureHotkeys.Name = nameof(btnConfigureHotkeys);\n            btnConfigureHotkeys.ClientRectangle = new Rectangle(lblPlayerName.X, lblNotice.Bottom + 36, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnConfigureHotkeys.Text = \"Configure Hotkeys\".L10N(\"Client:DTAConfig:ConfigureHotkeys\");\n            btnConfigureHotkeys.LeftClick += BtnConfigureHotkeys_LeftClick;\n\n            AddChild(lblScrollRate);\n            AddChild(lblScrollRateValue);\n            AddChild(trbScrollRate);\n            AddChild(chkScrollCoasting);\n            AddChild(chkTargetLines);\n            AddChild(chkTooltips);\n            AddChild(lblPlayerName);\n            AddChild(tbPlayerName);\n            AddChild(lblNotice);\n            AddChild(btnConfigureHotkeys);\n        }\n\n        private void BtnConfigureHotkeys_LeftClick(object sender, EventArgs e)\n        {\n            hotkeyConfigWindow.Enable();\n\n            if (topBar.Enabled)\n            {\n                topBar.Disable();\n                hotkeyConfigWindow.EnabledChanged += HotkeyConfigWindow_EnabledChanged;\n            }\n        }\n\n        private void HotkeyConfigWindow_EnabledChanged(object sender, EventArgs e)\n        {\n            hotkeyConfigWindow.EnabledChanged -= HotkeyConfigWindow_EnabledChanged;\n            topBar.Enable();\n        }\n\n        private void TrbScrollRate_ValueChanged(object sender, EventArgs e)\n        {\n            lblScrollRateValue.Text = trbScrollRate.Value.ToString();\n        }\n\n        public override void Load()\n        {\n            base.Load();\n            \n            int scrollRate = ReverseScrollRate(IniSettings.ScrollRate);\n\n            if (scrollRate >= trbScrollRate.MinValue && scrollRate <= trbScrollRate.MaxValue)\n            {\n                trbScrollRate.Value = scrollRate;\n                lblScrollRateValue.Text = scrollRate.ToString();\n            }\n\n            tbPlayerName.Text = UserINISettings.Instance.PlayerName;\n        }\n\n        public override bool Save()\n        {\n            bool restartRequired = base.Save();\n\n            IniSettings.ScrollRate.Value = ReverseScrollRate(trbScrollRate.Value);\n\n            string playerName = NameValidator.GetValidOfflineName(tbPlayerName.Text);\n\n            if (playerName.Length > 0)\n                IniSettings.PlayerName.Value = playerName;\n\n            return restartRequired;\n        }\n\n        private int ReverseScrollRate(int scrollRate)\n        {\n            return Math.Abs(scrollRate - MAX_SCROLL_RATE);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionPanels/UpdaterOptionsPanel.cs",
    "content": "﻿using ClientCore.Extensions;\nusing ClientCore;\nusing ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing ClientUpdater;\n\nnamespace DTAClient.DXGUI.Generic.OptionPanels\n{\n    class UpdaterOptionsPanel : XNAOptionsPanel\n    {\n        public UpdaterOptionsPanel(WindowManager windowManager, UserINISettings iniSettings)\n            : base(windowManager, iniSettings)\n        {\n        }\n\n        public event EventHandler OnForceUpdate;\n\n        private XNAListBox lbUpdateServerList;\n        private XNAClientCheckBox chkAutoCheck;\n        private XNAClientButton btnForceUpdate;\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"UpdaterOptionsPanel\";\n\n            var lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = \"lblDescription\";\n            lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            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\");\n\n            lbUpdateServerList = new XNAListBox(WindowManager);\n            lbUpdateServerList.Name = \"lblUpdateServerList\";\n            lbUpdateServerList.ClientRectangle = new Rectangle(lblDescription.X,\n                lblDescription.Bottom + 12, Width - 24, 100);\n            lbUpdateServerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2);\n            lbUpdateServerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            var btnMoveUp = new XNAClientButton(WindowManager);\n            btnMoveUp.Name = \"btnMoveUp\";\n            btnMoveUp.ClientRectangle = new Rectangle(lbUpdateServerList.X,\n                lbUpdateServerList.Bottom + 12, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnMoveUp.Text = \"Move Up\".L10N(\"Client:DTAConfig:MoveUp\");\n            btnMoveUp.LeftClick += btnMoveUp_LeftClick;\n\n            var btnMoveDown = new XNAClientButton(WindowManager);\n            btnMoveDown.Name = \"btnMoveDown\";\n            btnMoveDown.ClientRectangle = new Rectangle(\n                lbUpdateServerList.Right - UIDesignConstants.BUTTON_WIDTH_133,\n                btnMoveUp.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnMoveDown.Text = \"Move Down\".L10N(\"Client:DTAConfig:MoveDown\");\n            btnMoveDown.LeftClick += btnMoveDown_LeftClick;\n\n            chkAutoCheck = new XNAClientCheckBox(WindowManager);\n            chkAutoCheck.Name = \"chkAutoCheck\";\n            chkAutoCheck.ClientRectangle = new Rectangle(lblDescription.X,\n                btnMoveUp.Bottom + 24, 0, 0);\n            chkAutoCheck.Text = \"Check for updates automatically\".L10N(\"Client:DTAConfig:AutoCheckUpdate\");\n\n            btnForceUpdate = new XNAClientButton(WindowManager);\n            btnForceUpdate.Name = \"btnForceUpdate\";\n            btnForceUpdate.ClientRectangle = new Rectangle(btnMoveDown.X, btnMoveDown.Bottom + 24, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnForceUpdate.Text = \"Force Update\".L10N(\"Client:DTAConfig:ForceUpdate\");\n            btnForceUpdate.LeftClick += BtnForceUpdate_LeftClick;\n\n            AddChild(lblDescription);\n            AddChild(lbUpdateServerList);\n            AddChild(btnMoveUp);\n            AddChild(btnMoveDown);\n            AddChild(chkAutoCheck);\n            AddChild(btnForceUpdate);\n        }\n\n        private void BtnForceUpdate_LeftClick(object sender, EventArgs e)\n        {\n            var msgBox = new XNAMessageBox(WindowManager, \"Force Update Confirmation\".L10N(\"Client:DTAConfig:ForceUpdateConfirmTitle\"),\n                    (\"WARNING: Force update will result in files being re-verified\\n\" +\n                    \"and re-downloaded. While this may fix problems with game\\n\" +\n                    \"files, this also may delete some custom modifications\\n\" +\n                    \"made to this installation. Use at your own risk!\\n\\n\" +\n                    \"If you proceed, the options window will close and the\\n\" +\n                    \"client will proceed to checking for updates.\\n\\n\" +\n                    \"Do you really want to force update?\").L10N(\"Client:DTAConfig:ForceUpdateConfirmText\") + \"\\n\", XNAMessageBoxButtons.YesNo);\n            msgBox.Show();\n            msgBox.YesClickedAction = ForceUpdateMsgBox_YesClicked;\n        }\n\n        private void ForceUpdateMsgBox_YesClicked(XNAMessageBox obj)\n        {\n            Updater.ClearVersionInfo();\n            OnForceUpdate?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void btnMoveUp_LeftClick(object sender, EventArgs e)\n        {\n            int selectedIndex = lbUpdateServerList.SelectedIndex;\n\n            if (selectedIndex < 1)\n                return;\n\n            var tmp = lbUpdateServerList.Items[selectedIndex - 1];\n            lbUpdateServerList.Items[selectedIndex - 1] = lbUpdateServerList.Items[selectedIndex];\n            lbUpdateServerList.Items[selectedIndex] = tmp;\n\n            lbUpdateServerList.SelectedIndex--;\n\n            Updater.MoveMirrorUp(selectedIndex);\n        }\n\n        private void btnMoveDown_LeftClick(object sender, EventArgs e)\n        {\n            int selectedIndex = lbUpdateServerList.SelectedIndex;\n\n            if (selectedIndex > lbUpdateServerList.Items.Count - 2 || selectedIndex < 0)\n                return;\n\n            var tmp = lbUpdateServerList.Items[selectedIndex + 1];\n            lbUpdateServerList.Items[selectedIndex + 1] = lbUpdateServerList.Items[selectedIndex];\n            lbUpdateServerList.Items[selectedIndex] = tmp;\n\n            lbUpdateServerList.SelectedIndex++;\n\n            Updater.MoveMirrorDown(selectedIndex);\n        }\n\n        public override void Load()\n        {\n            base.Load();\n\n            lbUpdateServerList.Clear();\n\n            foreach (var updaterMirror in Updater.UpdateMirrors)\n            {\n\n                string name = updaterMirror.Name.L10N($\"INI:UpdateMirrors:{updaterMirror.Name}:Name\");\n                string location = updaterMirror.Location.L10N($\"INI:UpdateMirrors:{updaterMirror.Name}:Location\");\n\n                lbUpdateServerList.AddItem(name +\n                    (!string.IsNullOrEmpty(location)\n                        ? $\" ({location})\"\n                        : string.Empty));\n            }\n\n            chkAutoCheck.Checked = IniSettings.CheckForUpdates;\n        }\n\n        public override bool Save()\n        {\n            bool restartRequired = base.Save();\n\n            IniSettings.CheckForUpdates.Value = chkAutoCheck.Checked;\n\n            IniSettings.SettingsIni.EraseSectionKeys(\"DownloadMirrors\");\n\n            int id = 0;\n\n            foreach (UpdateMirror um in Updater.UpdateMirrors)\n            {\n                IniSettings.SettingsIni.SetStringValue(\"DownloadMirrors\", id.ToString(), um.Name);\n                id++;\n            }\n\n            return restartRequired;\n        }\n\n        public override void ToggleMainMenuOnlyOptions(bool enable)\n        {\n            btnForceUpdate.AllowClick = enable;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/OptionsWindow.cs",
    "content": "﻿using ClientCore.Extensions;\nusing ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Enums;\nusing ClientGUI;\nusing DTAClient.DXGUI.Generic.OptionPanels;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing ClientUpdater;\nusing DTAClient.Domain;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    public class OptionsWindow : XNAWindow\n    {\n        public OptionsWindow(WindowManager windowManager, GameCollection gameCollection, DirectDrawWrapperManager directDrawWrapperManager) : base(windowManager)\n        {\n            this.gameCollection = gameCollection;\n            this.directDrawWrapperManager = directDrawWrapperManager;\n        }\n\n        public event EventHandler OnForceUpdate;\n\n        private XNAClientTabControl tabControl;\n\n        private XNAOptionsPanel[] optionsPanels;\n        private ComponentsPanel componentsPanel;\n\n        private DisplayOptionsPanel displayOptionsPanel;\n        private XNAControl topBar;\n\n        private readonly GameCollection gameCollection;\n        private readonly DirectDrawWrapperManager directDrawWrapperManager;\n\n        public override void Initialize()\n        {\n            Name = \"OptionsWindow\";\n            ClientRectangle = new Rectangle(0, 0, 576, 475);\n            BackgroundTexture = AssetLoader.LoadTextureUncached(\"optionsbg.png\");\n\n            tabControl = new XNAClientTabControl(WindowManager);\n            tabControl.Name = \"tabControl\";\n            tabControl.ClientRectangle = new Rectangle(12, 12, 0, 23);\n            tabControl.FontIndex = 1;\n            tabControl.ClickSound = new EnhancedSoundEffect(\"button.wav\");\n            tabControl.AddTab(\"Display\".L10N(\"Client:DTAConfig:TabDisplay\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.AddTab(\"Audio\".L10N(\"Client:DTAConfig:TabAudio\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.AddTab(\"Game\".L10N(\"Client:DTAConfig:TabGame\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.AddTab(\"CnCNet\".L10N(\"Client:DTAConfig:TabCnCNet\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.AddTab(\"Updater\".L10N(\"Client:DTAConfig:TabUpdater\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.AddTab(\"Components\".L10N(\"Client:DTAConfig:TabComponents\"), UIDesignConstants.BUTTON_WIDTH_92);\n            tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged;\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = \"btnCancel\";\n            btnCancel.ClientRectangle = new Rectangle(Width - 104,\n                Height - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:DTAConfig:ButtonCancel\");\n            btnCancel.LeftClick += BtnBack_LeftClick;\n\n            var btnSave = new XNAClientButton(WindowManager);\n            btnSave.Name = \"btnSave\";\n            btnSave.ClientRectangle = new Rectangle(12, btnCancel.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnSave.Text = \"Save\".L10N(\"Client:DTAConfig:ButtonSave\");\n            btnSave.LeftClick += BtnSave_LeftClick;\n\n            displayOptionsPanel = new DisplayOptionsPanel(WindowManager, UserINISettings.Instance, directDrawWrapperManager);\n            componentsPanel = new ComponentsPanel(WindowManager, UserINISettings.Instance);\n            var updaterOptionsPanel = new UpdaterOptionsPanel(WindowManager, UserINISettings.Instance);\n            updaterOptionsPanel.OnForceUpdate += (s, e) => { Disable(); OnForceUpdate?.Invoke(this, EventArgs.Empty); };\n\n            optionsPanels = new XNAOptionsPanel[]\n            {\n                displayOptionsPanel,\n                new AudioOptionsPanel(WindowManager, UserINISettings.Instance),\n                new GameOptionsPanel(WindowManager, UserINISettings.Instance, topBar),\n                new CnCNetOptionsPanel(WindowManager, UserINISettings.Instance, gameCollection),\n                updaterOptionsPanel,\n                componentsPanel\n            };\n\n            if (ClientConfiguration.Instance.ModMode || Updater.UpdateMirrors == null || Updater.UpdateMirrors.Count < 1)\n            {\n                tabControl.MakeUnselectable(4);\n                tabControl.MakeUnselectable(5);\n            }\n            else if (Updater.CustomComponents == null || Updater.CustomComponents.Count < 1)\n                tabControl.MakeUnselectable(5);\n\n            foreach (var panel in optionsPanels)\n            {\n                AddChild(panel);\n                panel.Load();\n                panel.Disable();\n            }\n\n            optionsPanels[0].Enable();\n\n            AddChild(tabControl);\n            AddChild(btnCancel);\n            AddChild(btnSave);\n\n            base.Initialize();\n\n            CenterOnParent();\n        }\n\n        public void SetTopBar(XNAControl topBar) => this.topBar = topBar;\n\n        /// <summary>\n        /// Parses extra options defined by the modder\n        /// from an INI file. Called from XNAWindow.SetAttributesFromINI.\n        /// </summary>\n        /// <param name=\"iniFile\">The INI file.</param>\n        protected override void GetINIAttributes(IniFile iniFile)\n        {\n            base.GetINIAttributes(iniFile);\n\n            foreach (var panel in optionsPanels)\n                panel.ParseUserOptions(iniFile);\n        }\n\n        private void TabControl_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            foreach (var panel in optionsPanels)\n                panel.Disable();\n\n            optionsPanels[tabControl.SelectedTab].Enable();\n            optionsPanels[tabControl.SelectedTab].RefreshPanel();\n        }\n\n        private void BtnBack_LeftClick(object sender, EventArgs e)\n        {\n            if (Updater.IsComponentDownloadInProgress())\n            {\n                var msgBox = new XNAMessageBox(WindowManager, \"Downloads in progress\".L10N(\"Client:DTAConfig:DownloadingTitle\"),\n                    (\"Optional component downloads are in progress. The downloads will be cancelled if you exit the Options menu.\\n\\n\" +\n                    \"Are you sure you want to continue?\").L10N(\"Client:DTAConfig:DownloadingText\"), XNAMessageBoxButtons.YesNo);\n                msgBox.Show();\n                msgBox.YesClickedAction = ExitDownloadCancelConfirmation_YesClicked;\n\n                return;\n            }\n\n            WindowManager.SoundPlayer.SetVolume(Convert.ToSingle(UserINISettings.Instance.ClientVolume));\n            Disable();\n        }\n\n        private void ExitDownloadCancelConfirmation_YesClicked(XNAMessageBox messageBox)\n        {\n            componentsPanel.CancelAllDownloads();\n            WindowManager.SoundPlayer.SetVolume(Convert.ToSingle(UserINISettings.Instance.ClientVolume));\n            Disable();\n        }\n\n        private void BtnSave_LeftClick(object sender, EventArgs e)\n        {\n            if (Updater.IsComponentDownloadInProgress())\n            {\n                XNAMessageBox msgBox = new XNAMessageBox(WindowManager, \"Downloads in progress\".L10N(\"Client:DTAConfig:DownloadingTitle\"),\n                      (\"Optional component downloads are in progress. The downloads will be cancelled if you exit the Options menu.\\n\\n\" +\n                      \"Are you sure you want to continue?\").L10N(\"Client:DTAConfig:DownloadingText\"), XNAMessageBoxButtons.YesNo);\n                msgBox.Show();\n                msgBox.YesClickedAction = SaveDownloadCancelConfirmation_YesClicked;\n\n                return;\n            }\n\n            SaveSettings();\n        }\n\n        private void SaveDownloadCancelConfirmation_YesClicked(XNAMessageBox messageBox)\n        {\n            componentsPanel.CancelAllDownloads();\n\n            SaveSettings();\n        }\n\n        private void SaveSettings()\n        {\n            if (RefreshOptionPanels())\n                return;\n\n            bool restartRequired = false;\n\n            try\n            {\n                foreach (var panel in optionsPanels)\n                    restartRequired = panel.Save() || restartRequired;\n\n                UserINISettings.Instance.SaveSettings();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Saving settings failed! Error message: \" + ex.ToString());\n                XNAMessageBox.Show(WindowManager, \"Saving Settings Failed\".L10N(\"Client:DTAConfig:SaveSettingFailTitle\"),\n                    \"Saving settings failed! Error message:\".L10N(\"Client:DTAConfig:SaveSettingFailText\") + \" \" + ex.Message);\n            }\n\n            Disable();\n\n            if (restartRequired)\n            {\n                var msgBox = new XNAMessageBox(WindowManager, \"Restart Required\".L10N(\"Client:DTAConfig:RestartClientTitle\"),\n                    (\"The client needs to be restarted for some of the changes to take effect.\\n\\n\" +\n                    \"Do you want to restart now?\").L10N(\"Client:DTAConfig:RestartClientText\"), XNAMessageBoxButtons.YesNo);\n                msgBox.Show();\n                msgBox.YesClickedAction = RestartMsgBox_YesClicked;\n            }\n        }\n\n        private void RestartMsgBox_YesClicked(XNAMessageBox messageBox) => WindowManager.RestartGame();\n\n        /// <summary>\n        /// Refreshes the option panels to account for possible\n        /// changes that could affect theirs functionality.\n        /// Shows the popup to inform the user if needed.\n        /// </summary>\n        /// <returns>A bool that determines whether the \n        /// settings values were changed.</returns>\n        private bool RefreshOptionPanels()\n        {\n            bool optionValuesChanged = false;\n\n            foreach (var panel in optionsPanels)\n                optionValuesChanged = panel.RefreshPanel() || optionValuesChanged;\n\n            if (optionValuesChanged)\n            {\n                XNAMessageBox.Show(WindowManager, \"Setting Value(s) Changed\".L10N(\"Client:DTAConfig:SettingChangedTitle\"),\n                    (\"One or more setting values are\\n\" +\n                    \"no longer available and were changed.\\n\\n\" +\n                    \"You may want to verify the new setting\\n\" +\n                    \"values in client's options window.\").L10N(\"Client:DTAConfig:SettingChangedText\"));\n\n                return true;\n            }\n\n            return false;\n        }\n\n        public void RefreshSettings()\n        {\n            foreach (var panel in optionsPanels)\n                panel.Load();\n\n            RefreshOptionPanels();\n\n            foreach (var panel in optionsPanels)\n                panel.Save();\n\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        public void Open()\n        {\n            foreach (var panel in optionsPanels)\n                panel.Load();\n\n            RefreshOptionPanels();\n\n            componentsPanel.Open();\n\n            Enable();\n        }\n\n        public void ToggleMainMenuOnlyOptions(bool enable)\n        {\n            foreach (var panel in optionsPanels)\n            {\n                panel.ToggleMainMenuOnlyOptions(enable);\n            }\n        }\n\n        public void SwitchToCustomComponentsPanel()\n        {\n            foreach (var panel in optionsPanels)\n                panel.Disable();\n\n            tabControl.SelectedTab = 5;\n        }\n\n        public void InstallCustomComponent(int id) => componentsPanel.InstallComponent(id);\n\n        public void PostInit()\n        {\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n                displayOptionsPanel.PostInit();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/PrivacyNotification.cs",
    "content": "﻿using ClientCore;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// A notification that asks the user to accept the CnCNet privacy policy.\n    /// </summary>\n    class PrivacyNotification : XNAWindow\n    {\n        public PrivacyNotification(WindowManager windowManager) : base(windowManager)\n        {\n            // DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET;\n        }\n\n        public override void Initialize()\n        {\n            Name = nameof(PrivacyNotification);\n            Width = WindowManager.RenderResolutionX;\n\n            var lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES;\n            lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP;\n            lblDescription.Text = Renderer.FixText(\n                \"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\"),\n                lblDescription.FontIndex, WindowManager.RenderResolutionX - (UIDesignConstants.EMPTY_SPACE_SIDES * 2)).Text;\n            AddChild(lblDescription);\n\n            var lblMoreInformation = new XNALabel(WindowManager);\n            lblMoreInformation.Name = nameof(lblMoreInformation);\n            lblMoreInformation.X = lblDescription.X;\n            lblMoreInformation.Y = lblDescription.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN;\n            lblMoreInformation.Text = \"More information:\".L10N(\"Client:Main:TOSMoreInfo\")+ \" \";\n            AddChild(lblMoreInformation);\n\n            var lblTermsAndConditions = new XNALinkLabel(WindowManager);\n            lblTermsAndConditions.Name = nameof(lblTermsAndConditions);\n            lblTermsAndConditions.X = lblMoreInformation.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            lblTermsAndConditions.Y = lblMoreInformation.Y;\n            lblTermsAndConditions.Text = \"https://cncnet.org/terms-and-conditions\";\n            lblTermsAndConditions.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblTermsAndConditions.Text);\n            AddChild(lblTermsAndConditions);\n\n            var lblPrivacyPolicy = new XNALinkLabel(WindowManager);\n            lblPrivacyPolicy.Name = nameof(lblPrivacyPolicy);\n            lblPrivacyPolicy.X = lblTermsAndConditions.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            lblPrivacyPolicy.Y = lblMoreInformation.Y;\n            lblPrivacyPolicy.Text = \"https://cncnet.org/privacy-policy\";\n            lblPrivacyPolicy.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblPrivacyPolicy.Text);\n            AddChild(lblPrivacyPolicy);\n\n            var lblExplanation = new XNALabel(WindowManager);\n            lblExplanation.Name = nameof(lblExplanation);\n            lblExplanation.X = UIDesignConstants.EMPTY_SPACE_SIDES;\n            lblExplanation.Y = lblMoreInformation.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 2;\n            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\");\n            lblExplanation.TextColor = UISettings.ActiveSettings.SubtleTextColor;\n            AddChild(lblExplanation);\n\n            var btnOK = new XNAClientButton(WindowManager);\n            btnOK.Name = nameof(btnOK);\n            btnOK.Width = 75;\n            btnOK.Y = lblExplanation.Y;\n            btnOK.X = WindowManager.RenderResolutionX - btnOK.Width - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            btnOK.Text = \"Got it\".L10N(\"Client:Main:TOSButtonOK\");\n            AddChild(btnOK);\n            btnOK.LeftClick += (s, e) => \n            {\n                UserINISettings.Instance.PrivacyPolicyAccepted.Value = true;\n                UserINISettings.Instance.SaveSettings();\n                // AlphaRate = -0.2f;\n                Disable(); \n            };\n\n            Height = btnOK.Bottom + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n            Y = WindowManager.RenderResolutionY - Height;\n\n            base.Initialize();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            if (Alpha <= 0.0)\n                Disable();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/StatisticsWindow.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Statistics;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    public class StatisticsWindow : XNAWindow\n    {\n        public StatisticsWindow(WindowManager windowManager, MapLoader mapLoader)\n            : base(windowManager)\n        {\n            this.mapLoader = mapLoader;\n        }\n\n        private XNAPanel panelGameStatistics;\n        private XNAPanel panelTotalStatistics;\n\n        private XNAClientDropDown cmbGameModeFilter;\n        private XNAClientDropDown cmbGameClassFilter;\n\n        private XNAClientCheckBox chkIncludeSpectatedGames;\n\n        private XNAClientTabControl tabControl;\n\n        // Controls for game statistics\n\n        private XNAMultiColumnListBox lbGameList;\n        private XNAMultiColumnListBox lbGameStatistics;\n\n        private Texture2D[] sideTextures;\n\n        // *****************************\n\n        private const int TOTAL_STATS_LOCATION_X1 = 40;\n        private const int TOTAL_STATS_VALUE_LOCATION_X1 = 240;\n        private const int TOTAL_STATS_LOCATION_X2 = 380;\n        private const int TOTAL_STATS_VALUE_LOCATION_X2 = 580;\n        private const int TOTAL_STATS_Y_INCREASE = 45;\n        private const int TOTAL_STATS_FIRST_ITEM_Y = 20;\n\n        // Controls for total statistics\n\n        private XNALabel lblGamesStartedValue;\n        private XNALabel lblGamesFinishedValue;\n        private XNALabel lblWinsValue;\n        private XNALabel lblLossesValue;\n        private XNALabel lblWinLossRatioValue;\n        private XNALabel lblAverageGameLengthValue;\n        private XNALabel lblTotalTimePlayedValue;\n        private XNALabel lblAverageEnemyCountValue;\n        private XNALabel lblAverageAllyCountValue;\n        private XNALabel lblTotalKillsValue;\n        private XNALabel lblKillsPerGameValue;\n        private XNALabel lblTotalLossesValue;\n        private XNALabel lblLossesPerGameValue;\n        private XNALabel lblKillLossRatioValue;\n        private XNALabel lblTotalScoreValue;\n        private XNALabel lblAverageEconomyValue;\n        private XNALabel lblFavouriteSideValue;\n        private XNALabel lblAverageAILevelValue;\n\n        // *****************************\n\n        private StatisticsManager sm;\n        private MapLoader mapLoader;\n        private List<int> listedGameIndexes = new List<int>();\n\n        private (string Name, string UIName)[] sides;\n\n        private List<MultiplayerColor> mpColors;\n\n        private bool initialized = false;\n\n        public override void Initialize()\n        {\n            sm = StatisticsManager.Instance;\n\n            string strLblEconomy = \"ECONOMY\".L10N(\"Client:Main:StatisticEconomy\");\n            string strLblAvgEconomy = \"Average economy:\".L10N(\"Client:Main:StatisticEconomyAvg\");\n            if (ClientConfiguration.Instance.UseBuiltStatistic)\n            {\n                strLblEconomy = \"BUILT\".L10N(\"Client:Main:StatisticBuildCount\");\n                strLblAvgEconomy = \"Avg. number of objects built:\".L10N(\"Client:Main:StatisticBuildCountAvg\");\n            }\n\n            Name = \"StatisticsWindow\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"scoreviewerbg.png\");\n            ClientRectangle = new Rectangle(0, 0, 700, 521);\n            VisibleChanged += StatisticsWindow_VisibleChanged;\n\n            tabControl = new XNAClientTabControl(WindowManager);\n            tabControl.Name = nameof(tabControl);\n            tabControl.ClientRectangle = new Rectangle(12, 10, 0, 0);\n            tabControl.ClickSound = new EnhancedSoundEffect(\"button.wav\");\n            tabControl.FontIndex = 1;\n            tabControl.AddTab(\"Game Statistics\".L10N(\"Client:Main:GameStatistic\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.AddTab(\"Total Statistics\".L10N(\"Client:Main:TotalStatistic\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged;\n\n            XNALabel lblFilter = new XNALabel(WindowManager);\n            lblFilter.Name = nameof(lblFilter);\n            lblFilter.FontIndex = 1;\n            lblFilter.Text = \"FILTER:\".L10N(\"Client:Main:Filter\");\n            lblFilter.ClientRectangle = new Rectangle(527, 12, 0, 0);\n\n            cmbGameClassFilter = new XNAClientDropDown(WindowManager);\n            cmbGameClassFilter.ClientRectangle = new Rectangle(585, 11, 105, 21);\n            cmbGameClassFilter.Name = nameof(cmbGameClassFilter);\n            cmbGameClassFilter.AddItem(\"All games\".L10N(\"Client:Main:FilterAll\"));\n            cmbGameClassFilter.AddItem(\"Online games\".L10N(\"Client:Main:FilterOnline\"));\n            cmbGameClassFilter.AddItem(\"Online PvP\".L10N(\"Client:Main:FilterPvP\"));\n            cmbGameClassFilter.AddItem(\"Online Co-Op\".L10N(\"Client:Main:FilterCoOp\"));\n            cmbGameClassFilter.AddItem(\"Skirmish\".L10N(\"Client:Main:FilterSkirmish\"));\n            cmbGameClassFilter.SelectedIndex = 0;\n            cmbGameClassFilter.SelectedIndexChanged += CmbGameClassFilter_SelectedIndexChanged;\n\n            XNALabel lblGameMode = new XNALabel(WindowManager);\n            lblGameMode.Name = nameof(lblGameMode);\n            lblGameMode.FontIndex = 1;\n            lblGameMode.Text = \"GAME MODE:\".L10N(\"Client:Main:GameMode\");\n            lblGameMode.ClientRectangle = new Rectangle(294, 12, 0, 0);\n\n            cmbGameModeFilter = new XNAClientDropDown(WindowManager);\n            cmbGameModeFilter.Name = nameof(cmbGameModeFilter);\n            cmbGameModeFilter.ClientRectangle = new Rectangle(381, 11, 114, 21);\n            cmbGameModeFilter.SelectedIndexChanged += CmbGameModeFilter_SelectedIndexChanged;\n\n            var btnReturnToMenu = new XNAClientButton(WindowManager);\n            btnReturnToMenu.Name = nameof(btnReturnToMenu);\n            btnReturnToMenu.ClientRectangle = new Rectangle(270, 486, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnReturnToMenu.Text = \"Return to Main Menu\".L10N(\"Client:Main:ReturnToMainMenu\");\n            btnReturnToMenu.LeftClick += BtnReturnToMenu_LeftClick;\n\n            var btnClearStatistics = new XNAClientButton(WindowManager);\n            btnClearStatistics.Name = nameof(btnClearStatistics);\n            btnClearStatistics.ClientRectangle = new Rectangle(12, 486, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnClearStatistics.Text = \"Clear Statistics\".L10N(\"Client:Main:ClearStatistics\");\n            btnClearStatistics.LeftClick += BtnClearStatistics_LeftClick;\n            btnClearStatistics.Visible = false;\n\n            chkIncludeSpectatedGames = new XNAClientCheckBox(WindowManager);\n\n            AddChild(chkIncludeSpectatedGames);\n            chkIncludeSpectatedGames.Name = nameof(chkIncludeSpectatedGames);\n            chkIncludeSpectatedGames.Text = \"Include spectated games\".L10N(\"Client:Main:IncludeSpectated\");\n            chkIncludeSpectatedGames.Checked = true;\n            chkIncludeSpectatedGames.ClientRectangle = new Rectangle(\n                Width - chkIncludeSpectatedGames.Width - 12,\n                cmbGameModeFilter.Bottom + 3,\n                chkIncludeSpectatedGames.Width, \n                chkIncludeSpectatedGames.Height);\n            chkIncludeSpectatedGames.CheckedChanged += ChkIncludeSpectatedGames_CheckedChanged;\n\n            #region Match statistics\n\n            panelGameStatistics = new XNAPanel(WindowManager);\n            panelGameStatistics.Name = \"panelGameStatistics\";\n            panelGameStatistics.BackgroundTexture = AssetLoader.LoadTexture(\"scoreviewerpanelbg.png\");\n            panelGameStatistics.ClientRectangle = new Rectangle(10, 55, 680, 425);\n\n            AddChild(panelGameStatistics);\n\n            XNALabel lblGames = new XNALabel(WindowManager);\n            lblGames.Name = nameof(lblGames);\n            lblGames.Text = \"GAMES:\".L10N(\"Client:Main:GameMatches\");\n            lblGames.FontIndex = 1;\n            lblGames.ClientRectangle = new Rectangle(4, 2, 0, 0);\n\n            lbGameList = new XNAMultiColumnListBox(WindowManager);\n            lbGameList.Name = nameof(lbGameList);\n            lbGameList.ClientRectangle = new Rectangle(2, 25, 676, 250);\n            lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbGameList.AddColumn(\"DATE / TIME\".L10N(\"Client:Main:GameMatchDateTimeColumnHeader\"), 130);\n            lbGameList.AddColumn(\"MAP\".L10N(\"Client:Main:GameMatchMapColumnHeader\"), 200);\n            lbGameList.AddColumn(\"GAME MODE\".L10N(\"Client:Main:GameMatchGameModeColumnHeader\"), 130);\n            lbGameList.AddColumn(\"FPS\".L10N(\"Client:Main:GameMatchFPSColumnHeader\"), 50);\n            lbGameList.AddColumn(\"DURATION\".L10N(\"Client:Main:GameMatchDurationColumnHeader\"), 76);\n            lbGameList.AddColumn(\"COMPLETED\".L10N(\"Client:Main:GameMatchCompletedColumnHeader\"), 90);\n            lbGameList.SelectedIndexChanged += LbGameList_SelectedIndexChanged;\n            lbGameList.AllowKeyboardInput = true;\n\n            lbGameStatistics = new XNAMultiColumnListBox(WindowManager);\n            lbGameStatistics.Name = nameof(lbGameStatistics);\n            lbGameStatistics.ClientRectangle = new Rectangle(2, 280, 676, 143);\n            lbGameStatistics.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbGameStatistics.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbGameStatistics.AddColumn(\"NAME\".L10N(\"Client:Main:StatisticsName\"), 130);\n            lbGameStatistics.AddColumn(\"KILLS\".L10N(\"Client:Main:StatisticsKills\"), 78);\n            lbGameStatistics.AddColumn(\"LOSSES\".L10N(\"Client:Main:StatisticsLosses\"), 78);\n            lbGameStatistics.AddColumn(strLblEconomy, 80);\n            lbGameStatistics.AddColumn(\"SCORE\".L10N(\"Client:Main:StatisticsScore\"), 100);\n            lbGameStatistics.AddColumn(\"WON\".L10N(\"Client:Main:StatisticsWon\"), 50);\n            lbGameStatistics.AddColumn(\"SIDE\".L10N(\"Client:Main:StatisticsSide\"), 100);\n            lbGameStatistics.AddColumn(\"TEAM\".L10N(\"Client:Main:StatisticsTeam\"), 60);\n\n            panelGameStatistics.AddChild(lblGames);\n            panelGameStatistics.AddChild(lbGameList);\n            panelGameStatistics.AddChild(lbGameStatistics);\n\n#endregion\n\n#region Total statistics\n\n            panelTotalStatistics = new XNAPanel(WindowManager);\n            panelTotalStatistics.Name = \"panelTotalStatistics\";\n            panelTotalStatistics.BackgroundTexture = AssetLoader.LoadTexture(\"scoreviewerpanelbg.png\");\n            panelTotalStatistics.ClientRectangle = new Rectangle(10, 55, 680, 425);\n\n            AddChild(panelTotalStatistics);\n            panelTotalStatistics.Visible = false;\n            panelTotalStatistics.Enabled = false;\n\n            int locationY = TOTAL_STATS_FIRST_ITEM_Y;\n\n            AddTotalStatisticsLabel(\"lblGamesStarted\", \"Games started:\".L10N(\"Client:Main:StatisticsGamesStarted\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblGamesStartedValue = new XNALabel(WindowManager);\n            lblGamesStartedValue.Name = \"lblGamesStartedValue\";\n            lblGamesStartedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblGamesStartedValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblGamesFinished\", \"Games finished:\".L10N(\"Client:Main:StatisticsGamesFinished\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblGamesFinishedValue = new XNALabel(WindowManager);\n            lblGamesFinishedValue.Name = \"lblGamesFinishedValue\";\n            lblGamesFinishedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblGamesFinishedValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblWins\", \"Wins:\".L10N(\"Client:Main:StatisticsGamesWins\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblWinsValue = new XNALabel(WindowManager);\n            lblWinsValue.Name = \"lblWinsValue\";\n            lblWinsValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblWinsValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblLosses\", \"Losses:\".L10N(\"Client:Main:StatisticsGamesLosses\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblLossesValue = new XNALabel(WindowManager);\n            lblLossesValue.Name = \"lblLossesValue\";\n            lblLossesValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblLossesValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblWinLossRatio\", \"Win / Loss ratio:\".L10N(\"Client:Main:StatisticsGamesWinLossRatio\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblWinLossRatioValue = new XNALabel(WindowManager);\n            lblWinLossRatioValue.Name = \"lblWinLossRatioValue\";\n            lblWinLossRatioValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblWinLossRatioValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblAverageGameLength\", \"Average game length:\".L10N(\"Client:Main:StatisticsGamesLengthAvg\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblAverageGameLengthValue = new XNALabel(WindowManager);\n            lblAverageGameLengthValue.Name = \"lblAverageGameLengthValue\";\n            lblAverageGameLengthValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblAverageGameLengthValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblTotalTimePlayed\", \"Total time played:\".L10N(\"Client:Main:StatisticsTotalTimePlayed\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblTotalTimePlayedValue = new XNALabel(WindowManager);\n            lblTotalTimePlayedValue.Name = \"lblTotalTimePlayedValue\";\n            lblTotalTimePlayedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblTotalTimePlayedValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblAverageEnemyCount\", \"Average number of enemies:\".L10N(\"Client:Main:StatisticsEnemiesAvg\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblAverageEnemyCountValue = new XNALabel(WindowManager);\n            lblAverageEnemyCountValue.Name = \"lblAverageEnemyCountValue\";\n            lblAverageEnemyCountValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblAverageEnemyCountValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblAverageAllyCount\", \"Average number of allies:\".L10N(\"Client:Main:StatisticsAlliesAvg\"), new Point(TOTAL_STATS_LOCATION_X1, locationY));\n\n            lblAverageAllyCountValue = new XNALabel(WindowManager);\n            lblAverageAllyCountValue.Name = \"lblAverageAllyCountValue\";\n            lblAverageAllyCountValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0);\n            lblAverageAllyCountValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            // SECOND COLUMN\n\n            locationY = TOTAL_STATS_FIRST_ITEM_Y;\n\n            AddTotalStatisticsLabel(\"lblTotalKills\", \"Total kills:\".L10N(\"Client:Main:StatisticsTotalKills\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblTotalKillsValue = new XNALabel(WindowManager);\n            lblTotalKillsValue.Name = \"lblTotalKillsValue\";\n            lblTotalKillsValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblTotalKillsValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblKillsPerGame\", \"Kills / game:\".L10N(\"Client:Main:StatisticsKillsPerGame\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblKillsPerGameValue = new XNALabel(WindowManager);\n            lblKillsPerGameValue.Name = \"lblKillsPerGameValue\";\n            lblKillsPerGameValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblKillsPerGameValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblTotalLosses\", \"Total losses:\".L10N(\"Client:Main:StatisticsTotalLosses\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblTotalLossesValue = new XNALabel(WindowManager);\n            lblTotalLossesValue.Name = \"lblTotalLossesValue\";\n            lblTotalLossesValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblTotalLossesValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblLossesPerGame\", \"Losses / game:\".L10N(\"Client:Main:StatisticsLossesPerGame\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblLossesPerGameValue = new XNALabel(WindowManager);\n            lblLossesPerGameValue.Name = \"lblLossesPerGameValue\";\n            lblLossesPerGameValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblLossesPerGameValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblKillLossRatio\", \"Kill / loss ratio:\".L10N(\"Client:Main:StatisticsKillLossRatio\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblKillLossRatioValue = new XNALabel(WindowManager);\n            lblKillLossRatioValue.Name = \"lblKillLossRatioValue\";\n            lblKillLossRatioValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblKillLossRatioValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblTotalScore\", \"Total score:\".L10N(\"Client:Main:TotalScore\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblTotalScoreValue = new XNALabel(WindowManager);\n            lblTotalScoreValue.Name = \"lblTotalScoreValue\";\n            lblTotalScoreValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblTotalScoreValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblAverageEconomy\", strLblAvgEconomy, new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblAverageEconomyValue = new XNALabel(WindowManager);\n            lblAverageEconomyValue.Name = \"lblAverageEconomyValue\";\n            lblAverageEconomyValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblAverageEconomyValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblFavouriteSide\", \"Favourite side:\".L10N(\"Client:Main:FavouriteSide\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblFavouriteSideValue = new XNALabel(WindowManager);\n            lblFavouriteSideValue.Name = \"lblFavouriteSideValue\";\n            lblFavouriteSideValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblFavouriteSideValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            AddTotalStatisticsLabel(\"lblAverageAILevel\", \"Average AI level:\".L10N(\"Client:Main:AvgAILevel\"), new Point(TOTAL_STATS_LOCATION_X2, locationY));\n\n            lblAverageAILevelValue = new XNALabel(WindowManager);\n            lblAverageAILevelValue.Name = \"lblAverageAILevelValue\";\n            lblAverageAILevelValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0);\n            lblAverageAILevelValue.RemapColor = UISettings.ActiveSettings.AltColor;\n            locationY += TOTAL_STATS_Y_INCREASE;\n\n            panelTotalStatistics.AddChild(lblGamesStartedValue);\n            panelTotalStatistics.AddChild(lblGamesFinishedValue);\n            panelTotalStatistics.AddChild(lblWinsValue);\n            panelTotalStatistics.AddChild(lblLossesValue);\n            panelTotalStatistics.AddChild(lblWinLossRatioValue);\n            panelTotalStatistics.AddChild(lblAverageGameLengthValue);\n            panelTotalStatistics.AddChild(lblTotalTimePlayedValue);\n            panelTotalStatistics.AddChild(lblAverageEnemyCountValue);\n            panelTotalStatistics.AddChild(lblAverageAllyCountValue);\n\n            panelTotalStatistics.AddChild(lblTotalKillsValue);\n            panelTotalStatistics.AddChild(lblKillsPerGameValue);\n            panelTotalStatistics.AddChild(lblTotalLossesValue);\n            panelTotalStatistics.AddChild(lblLossesPerGameValue);\n            panelTotalStatistics.AddChild(lblKillLossRatioValue);\n            panelTotalStatistics.AddChild(lblTotalScoreValue);\n            panelTotalStatistics.AddChild(lblAverageEconomyValue);\n            panelTotalStatistics.AddChild(lblFavouriteSideValue);\n            panelTotalStatistics.AddChild(lblAverageAILevelValue);\n\n#endregion\n\n            AddChild(tabControl);\n            AddChild(lblFilter);\n            AddChild(cmbGameClassFilter);\n            AddChild(lblGameMode);\n            AddChild(cmbGameModeFilter);\n            AddChild(btnReturnToMenu);\n            AddChild(btnClearStatistics);\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            sides = ClientConfiguration.Instance.Sides.Split(',')\n                .Select(s => (Name: s, UIName: s.L10N($\"INI:Sides:{s}\"))).ToArray();\n\n            sideTextures = new Texture2D[sides.Length + 1];\n            for (int i = 0; i < sides.Length; i++)\n                sideTextures[i] = AssetLoader.LoadTexture(sides[i].Name + \"icon.png\");\n\n            sideTextures[sides.Length] = AssetLoader.LoadTexture(\"spectatoricon.png\");\n\n            mpColors = MultiplayerColor.LoadColors();\n\n            ReadStatistics();\n            ListGameModes();\n\n            StatisticsManager.Instance.GameAdded += Instance_GameAdded;\n\n            initialized = true;\n        }\n\n        private void StatisticsWindow_VisibleChanged(object sender, EventArgs e)\n        {\n            ListGames();\n        }\n\n        private void Instance_GameAdded(object sender, EventArgs e)\n        {\n            ListGames();\n        }\n\n        private void ChkIncludeSpectatedGames_CheckedChanged(object sender, EventArgs e)\n        {\n            ListGames();\n        }\n\n        private void AddTotalStatisticsLabel(string name, string text, Point location)\n        {\n            XNALabel label = new XNALabel(WindowManager);\n            label.Name = name;\n            label.Text = text;\n            label.ClientRectangle = new Rectangle(location.X, location.Y, 0, 0);\n            panelTotalStatistics.AddChild(label);\n        }\n\n        private void TabControl_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (tabControl.SelectedTab == 1)\n            {\n                panelGameStatistics.Visible = false;\n                panelGameStatistics.Enabled = false;\n                panelTotalStatistics.Visible = true;\n                panelTotalStatistics.Enabled = true;\n            }\n            else\n            {\n                panelGameStatistics.Visible = true;\n                panelGameStatistics.Enabled = true;\n                panelTotalStatistics.Visible = false;\n                panelTotalStatistics.Enabled = false;\n            }\n        }\n\n        private void CmbGameClassFilter_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            ListGames();\n        }\n\n        private void CmbGameModeFilter_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            ListGames();\n        }\n\n        private void LbGameList_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            lbGameStatistics.ClearItems();\n\n            if (lbGameList.SelectedIndex == -1)\n                return;\n\n            MatchStatistics ms = sm.GetMatchByIndex(listedGameIndexes[lbGameList.SelectedIndex]);\n\n            List<PlayerStatistics> players = new List<PlayerStatistics>();\n\n            for (int i = 0; i < ms.GetPlayerCount(); i++)\n            {\n                players.Add(ms.GetPlayer(i));\n            }\n\n            players = players.OrderBy(p => p.Score).Reverse().ToList();\n\n            Color textColor = UISettings.ActiveSettings.AltColor;\n\n            for (int i = 0; i < ms.GetPlayerCount(); i++)\n            {\n                PlayerStatistics ps = players[i];\n\n                //List<string> items = new List<string>();\n                List<XNAListBoxItem> items = new List<XNAListBoxItem>();\n\n                if (ps.Color > -1 && ps.Color < mpColors.Count)\n                    textColor = mpColors[ps.Color].XnaColor;\n\n                if (ps.IsAI)\n                {\n                    items.Add(new XNAListBoxItem(ProgramConstants.GetAILevelName(ps.AILevel), textColor));\n                }\n                else\n                    items.Add(new XNAListBoxItem(ps.Name, textColor));\n\n                if (ps.WasSpectator)\n                {\n                    // Player was a spectator\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                    XNAListBoxItem spectatorItem = new XNAListBoxItem();\n                    spectatorItem.Text = \"Spectator\".L10N(\"Client:Main:Spectator\");\n                    spectatorItem.TextColor = textColor;\n                    spectatorItem.Texture = sideTextures[sideTextures.Length - 1];\n                    items.Add(spectatorItem);\n                    items.Add(new XNAListBoxItem(\"-\", textColor));\n                }\n                else\n                { \n                    if (!ms.SawCompletion)\n                    {\n                        // The game wasn't completed - we don't know the stats\n                        items.Add(new XNAListBoxItem(\"-\", textColor));\n                        items.Add(new XNAListBoxItem(\"-\", textColor));\n                        items.Add(new XNAListBoxItem(\"-\", textColor));\n                        items.Add(new XNAListBoxItem(\"-\", textColor));\n                        items.Add(new XNAListBoxItem(\"-\", textColor));\n                    }\n                    else\n                    {\n                        // The game was completed and the player was actually playing\n                        items.Add(new XNAListBoxItem(ps.Kills.ToString(), textColor));\n                        items.Add(new XNAListBoxItem(ps.Losses.ToString(), textColor));\n                        items.Add(new XNAListBoxItem(ps.Economy.ToString(), textColor));\n                        items.Add(new XNAListBoxItem(ps.Score.ToString(), textColor));\n                        items.Add(new XNAListBoxItem(\n                            Conversions.BooleanToString(ps.Won, BooleanStringStyle.YESNO), textColor));\n                    }\n\n                    if (ps.Side == 0 || ps.Side > sides.Length)\n                        items.Add(new XNAListBoxItem(\"Unknown\".L10N(\"Client:Main:UnknownSide\"), textColor));\n                    else\n                    {\n                        XNAListBoxItem sideItem = new XNAListBoxItem();\n                        sideItem.Text = sides[ps.Side - 1].UIName;\n                        sideItem.TextColor = textColor;\n                        sideItem.Texture = sideTextures[ps.Side - 1];\n                        items.Add(sideItem);\n                    }\n\n                    items.Add(new XNAListBoxItem(TeamIndexToString(ps.Team), textColor));\n                }\n\n                if (!ps.IsLocalPlayer)\n                {\n                    lbGameStatistics.AddItem(items);\n\n                    items.ForEach(item => item.Selectable = false);\n                }\n                else\n                {\n                    lbGameStatistics.AddItem(items);\n                    lbGameStatistics.SelectedIndex = i;\n                }\n            }\n        }\n\n        private string TeamIndexToString(int teamIndex)\n        {\n            if (teamIndex < 1 || teamIndex >= ProgramConstants.TEAMS.Count)\n                return \"-\";\n\n            return ProgramConstants.TEAMS[teamIndex - 1];\n        }\n\n        #region Statistics reading / game listing code\n\n        private void ReadStatistics()\n        {\n            StatisticsManager sm = StatisticsManager.Instance;\n\n            sm.ReadStatistics(ProgramConstants.GamePath);\n        }\n\n        private void ListGameModes()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            List<string> gameModes = new List<string>();\n\n            cmbGameModeFilter.Items.Clear();\n\n            cmbGameModeFilter.AddItem(\"All\".L10N(\"Client:Main:AllGameModes\"));\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(i);\n                if (!gameModes.Contains(ms.GameMode))\n                    gameModes.Add(ms.GameMode);\n            }\n\n            gameModes.Sort();\n\n            foreach (string gm in gameModes)\n                cmbGameModeFilter.AddItem(new XNADropDownItem { Text = gm.L10N($\"INI:GameModes:{gm}:UIName\"), Tag = gm });\n\n            cmbGameModeFilter.SelectedIndex = 0;\n        }\n\n        private void ListGames()\n        {\n            if (!Visible || !initialized)\n                return;\n\n            lbGameList.SelectedIndex = -1;\n            lbGameList.SetTopIndex(0);\n\n            lbGameStatistics.ClearItems();\n            lbGameList.ClearItems();\n            listedGameIndexes.Clear();\n\n            switch (cmbGameClassFilter.SelectedIndex)\n            {\n                case 0:\n                    ListAllGames();\n                    break;\n                case 1:\n                    ListOnlineGames();\n                    break;\n                case 2:\n                    ListPvPGames();\n                    break;\n                case 3:\n                    ListCoOpGames();\n                    break;\n                case 4:\n                    ListSkirmishGames();\n                    break;\n            }\n\n            listedGameIndexes.Reverse();\n\n            SetTotalStatistics();\n\n            foreach (int gameIndex in listedGameIndexes)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(gameIndex);\n                string dateTime = ms.DateAndTime.ToShortDateString() + \" \" + ms.DateAndTime.ToShortTimeString();\n                List<string> info = new List<string>();\n                info.Add(Renderer.GetSafeString(dateTime, lbGameList.FontIndex));\n                info.Add(mapLoader.TranslatedMapNames.ContainsKey(ms.MapName)\n                    ? mapLoader.TranslatedMapNames[ms.MapName]\n                    : ms.MapName);\n                info.Add(ms.GameMode.L10N($\"INI:GameModes:{ms.GameMode}:UIName\"));\n                if (ms.AverageFPS == 0)\n                    info.Add(\"-\");\n                else\n                    info.Add(ms.AverageFPS.ToString());\n                info.Add(Renderer.GetSafeString(TimeSpan.FromSeconds(ms.LengthInSeconds).ToString(), lbGameList.FontIndex));\n                info.Add(Conversions.BooleanToString(ms.SawCompletion, BooleanStringStyle.YESNO));\n                lbGameList.AddItem(info, true);\n            }\n        }\n\n        private void ListAllGames()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                ListGameIndexIfPrerequisitesMet(i);\n            }\n        }\n\n        private void ListOnlineGames()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(i);\n\n                int pCount = ms.GetPlayerCount();\n                int hpCount = 0;\n\n                for (int j = 0; j < pCount; j++)\n                {\n                    PlayerStatistics ps = ms.GetPlayer(j);\n\n                    if (!ps.IsAI)\n                    {\n                        hpCount++;\n\n                        if (hpCount > 1)\n                        {\n                            ListGameIndexIfPrerequisitesMet(i);\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        private void ListPvPGames()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(i);\n\n                int pCount = ms.GetPlayerCount();\n                int pTeam = -1;\n\n                for (int j = 0; j < pCount; j++)\n                {\n                    PlayerStatistics ps = ms.GetPlayer(j);\n\n                    if (!ps.IsAI && !ps.WasSpectator)\n                    {\n                        // If we find a single player on a different team than another player,\n                        // we'll count the game as a PvP game\n                        if (pTeam > -1 && (ps.Team != pTeam || ps.Team == 0))\n                        {\n                            ListGameIndexIfPrerequisitesMet(i);\n                            break;\n                        }\n\n                        pTeam = ps.Team;\n                    }\n                }\n            }\n        }\n\n        private void ListCoOpGames()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(i);\n\n                int pCount = ms.GetPlayerCount();\n                int hpCount = 0;\n                int pTeam = -1;\n                bool add = true;\n\n                for (int j = 0; j < pCount; j++)\n                {\n                    PlayerStatistics ps = ms.GetPlayer(j);\n\n                    if (!ps.IsAI && !ps.WasSpectator)\n                    {\n                        hpCount++;\n\n                        if (pTeam > -1 && (ps.Team != pTeam || ps.Team == 0))\n                        {\n                            add = false;\n                            break;\n                        }\n\n                        pTeam = ps.Team;\n                    }\n                }\n\n                if (add && hpCount > 1)\n                {\n                    ListGameIndexIfPrerequisitesMet(i);\n                }\n            }\n        }\n\n        private void ListSkirmishGames()\n        {\n            int gameCount = sm.GetMatchCount();\n\n            for (int i = 0; i < gameCount; i++)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(i);\n\n                int pCount = ms.GetPlayerCount();\n                int hpCount = 0;\n                bool add = true;\n\n                foreach (PlayerStatistics ps in ms.Players)\n                {\n                    if (!ps.IsAI)\n                    {\n                        hpCount++;\n\n                        if (hpCount > 1)\n                        {\n                            add = false;\n                            break;\n                        }\n                    }\n                }\n\n                if (add)\n                {\n                    ListGameIndexIfPrerequisitesMet(i);\n                }\n            }\n        }\n\n        private void ListGameIndexIfPrerequisitesMet(int gameIndex)\n        {\n            MatchStatistics ms = sm.GetMatchByIndex(gameIndex);\n\n            if (cmbGameModeFilter.SelectedIndex != 0)\n            {\n                // \"All\" doesn't have a tag but that doesn't matter since 0 is not checked\n                var gameMode = (string)cmbGameModeFilter.Items[cmbGameModeFilter.SelectedIndex].Tag;\n\n                if (ms.GameMode != gameMode)\n                    return;\n            }\n\n            PlayerStatistics ps = ms.Players.Find(p => p.IsLocalPlayer);\n\n            if (ps != null && !chkIncludeSpectatedGames.Checked)\n            {\n                if (ps.WasSpectator)\n                    return;\n            }\n\n            listedGameIndexes.Add(gameIndex);\n        }\n\n        /// <summary>\n        /// Adjusts the labels on the \"Total statistics\" tab.\n        /// </summary>\n        private void SetTotalStatistics()\n        {\n            int gamesStarted = 0;\n            int gamesFinished = 0;\n            int gamesPlayed = 0;\n            int wins = 0;\n            int gameLosses = 0;\n            TimeSpan timePlayed = TimeSpan.Zero;\n            int numEnemies = 0;\n            int numAllies = 0;\n            int totalKills = 0;\n            int totalLosses = 0;\n            int totalScore = 0;\n            int totalEconomy = 0;\n            int[] sideGameCounts = new int[sides.Length];\n            int numEasyAIs = 0;\n            int numMediumAIs = 0;\n            int numHardAIs = 0;\n\n            foreach (int gameIndex in listedGameIndexes)\n            {\n                MatchStatistics ms = sm.GetMatchByIndex(gameIndex);\n\n                gamesStarted++;\n\n                if (ms.SawCompletion)\n                    gamesFinished++;\n\n                timePlayed += TimeSpan.FromSeconds(ms.LengthInSeconds);\n\n                PlayerStatistics localPlayer = FindLocalPlayer(ms);\n\n                if (!localPlayer.WasSpectator)\n                {\n                    totalKills += localPlayer.Kills;\n                    totalLosses += localPlayer.Losses;\n                    totalScore += localPlayer.Score;\n                    totalEconomy += localPlayer.Economy;\n\n                    if (localPlayer.Side > 0 && localPlayer.Side <= sides.Length)\n                        sideGameCounts[localPlayer.Side - 1]++;\n\n                    if (!ms.SawCompletion)\n                        continue;\n\n                    if (localPlayer.Won)\n                        wins++;\n                    else\n                        gameLosses++;\n\n                    gamesPlayed++;\n\n                    for (int i = 0; i < ms.GetPlayerCount(); i++)\n                    {\n                        PlayerStatistics ps = ms.GetPlayer(i);\n\n                        if (!ps.WasSpectator && (!ps.IsLocalPlayer || ps.IsAI))\n                        {\n                            if (ps.Team == 0 || localPlayer.Team != ps.Team)\n                                numEnemies++;\n                            else\n                                numAllies++;\n\n                            if (ps.IsAI)\n                            {\n                                if (ps.AILevel == 0)\n                                    numEasyAIs++;\n                                else if (ps.AILevel == 1)\n                                    numMediumAIs++;\n                                else\n                                    numHardAIs++;\n                            }\n                        }\n                    }\n                }\n            }\n\n            lblGamesStartedValue.Text = gamesStarted.ToString();\n            lblGamesFinishedValue.Text = gamesFinished.ToString();\n            lblWinsValue.Text = wins.ToString();\n            lblLossesValue.Text = gameLosses.ToString();\n\n            if (gameLosses > 0)\n            {\n                lblWinLossRatioValue.Text = Math.Round(wins / (double)gameLosses, 2).ToString();\n            }\n            else\n                lblWinLossRatioValue.Text = \"-\";\n\n            if (gamesStarted > 0)\n            {\n                lblAverageGameLengthValue.Text = TimeSpan.FromSeconds((int)timePlayed.TotalSeconds / gamesStarted).ToString();\n            }\n            else\n                lblAverageGameLengthValue.Text = \"-\";\n\n            if (gamesPlayed > 0)\n            {\n                lblAverageEnemyCountValue.Text = Math.Round(numEnemies / (double)gamesPlayed, 2).ToString();\n                lblAverageAllyCountValue.Text = Math.Round(numAllies / (double)gamesPlayed, 2).ToString();\n                lblKillsPerGameValue.Text = (totalKills / gamesPlayed).ToString();\n                lblLossesPerGameValue.Text = (totalLosses / gamesPlayed).ToString();\n                lblAverageEconomyValue.Text = (totalEconomy / gamesPlayed).ToString();\n            }\n            else\n            {\n                lblAverageEnemyCountValue.Text = \"-\";\n                lblAverageAllyCountValue.Text = \"-\";\n                lblKillsPerGameValue.Text = \"-\";\n                lblLossesPerGameValue.Text = \"-\";\n                lblAverageEconomyValue.Text = \"-\";\n            }\n\n            if (totalLosses > 0)\n            {\n                lblKillLossRatioValue.Text = Math.Round(totalKills / (double)totalLosses, 2).ToString();\n            }\n            else\n                lblKillLossRatioValue.Text = \"-\";\n\n            lblTotalTimePlayedValue.Text = timePlayed.ToString();\n            lblTotalKillsValue.Text = totalKills.ToString();\n            lblTotalLossesValue.Text = totalLosses.ToString();\n            lblTotalScoreValue.Text = totalScore.ToString();\n            lblFavouriteSideValue.Text = sides[GetHighestIndex(sideGameCounts)].UIName;\n\n            if (numEasyAIs >= numMediumAIs && numEasyAIs >= numHardAIs)\n                lblAverageAILevelValue.Text = \"Easy\".L10N(\"Client:Main:EasyAI\");\n            else if (numMediumAIs >= numEasyAIs && numMediumAIs >= numHardAIs)\n                lblAverageAILevelValue.Text = \"Medium\".L10N(\"Client:Main:MediumAI\");\n            else\n                lblAverageAILevelValue.Text = \"Hard\".L10N(\"Client:Main:HardAI\");\n        }\n\n        private PlayerStatistics FindLocalPlayer(MatchStatistics ms)\n        {\n            int pCount = ms.GetPlayerCount();\n\n            for (int pId = 0; pId < pCount; pId++)\n            {\n                PlayerStatistics ps = ms.GetPlayer(pId);\n\n                if (!ps.IsAI && ps.IsLocalPlayer)\n                    return ps;\n            }\n\n            return null;\n        }\n\n        private int GetHighestIndex(int[] t)\n        {\n            int highestIndex = -1;\n            int highest = Int32.MinValue;\n\n            for (int i = 0; i < t.Length; i++)\n            {\n                if (t[i] > highest)\n                {\n                    highest = t[i];\n                    highestIndex = i;\n                }\n            }\n\n            return highestIndex;\n        }\n\n        private void ClearAllStatistics()\n        {\n            StatisticsManager.Instance.ClearDatabase();\n            ReadStatistics();\n            ListGameModes();\n            ListGames();\n        }\n\n        #endregion\n\n        private void BtnReturnToMenu_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        private void BtnClearStatistics_LeftClick(object sender, EventArgs e)\n        {\n            var msgBox = new XNAMessageBox(WindowManager, \"Clear all statistics\".L10N(\"Client:Main:ClearStatisticsTitle\"),\n                (\"All statistics data will be cleared from the database.\\n\\nAre you sure you want to continue?\").L10N(\"Client:Main:ClearStatisticsText\"), XNAMessageBoxButtons.YesNo);\n            msgBox.Show();\n            msgBox.YesClickedAction = ClearStatisticsConfirmation_YesClicked;\n        }\n\n        private void ClearStatisticsConfirmation_YesClicked(XNAMessageBox messageBox)\n        {\n            ClearAllStatistics();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/TopBar.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing Rampastring.XNAUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI.Input;\nusing Microsoft.Xna.Framework.Input;\nusing DTAClient.Online;\nusing ClientGUI;\nusing ClientCore;\nusing System.Threading;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.Online.EventArguments;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// A top bar that allows switching between various client windows.\n    /// </summary>\n    public class TopBar : XNAPanel\n    {\n        /// <summary>\n        /// The number of seconds that the top bar will stay down after it has\n        /// lost input focus.\n        /// </summary>\n        const double DOWN_TIME_WAIT_SECONDS = 1.0;\n        const double EVENT_DOWN_TIME_WAIT_SECONDS = 2.0;\n        const double STARTUP_DOWN_TIME_WAIT_SECONDS = 3.5;\n\n        const double DOWN_MOVEMENT_RATE = 1.7;\n        const double UP_MOVEMENT_RATE = 1.7;\n        const int APPEAR_CURSOR_THRESHOLD_Y = 8;\n\n        private readonly string DEFAULT_PM_BTN_LABEL = \"Private Messages (F4)\".L10N(\"Client:Main:PMButtonF4\");\n\n        public TopBar(\n            WindowManager windowManager,\n            CnCNetManager connectionManager,\n            PrivateMessageHandler privateMessageHandler\n        ) : base(windowManager)\n        {\n            downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS);\n            this.connectionManager = connectionManager;\n            this.privateMessageHandler = privateMessageHandler;\n        }\n\n        public SwitchType LastSwitchType { get; private set; }\n\n        private List<ISwitchable> primarySwitches = new List<ISwitchable>();\n        private ISwitchable cncnetLobbySwitch;\n        private ISwitchable privateMessageSwitch;\n\n        private OptionsWindow optionsWindow;\n\n        private XNAClientButton btnMainButton;\n        private XNAClientButton btnCnCNetLobby;\n        private XNAClientButton btnPrivateMessages;\n        private XNAClientButton btnOptions;\n        private XNAClientButton btnLogout;\n        private XNALabel lblTime;\n        private XNALabel lblDate;\n        private XNALabel lblCnCNetStatus;\n        private XNALabel lblCnCNetPlayerCount;\n        private XNALabel lblConnectionStatus;\n\n        private CnCNetManager connectionManager;\n        private readonly PrivateMessageHandler privateMessageHandler;\n\n        private CancellationTokenSource cncnetPlayerCountCancellationSource;\n        private static readonly object locker = new object();\n\n        private TimeSpan downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - STARTUP_DOWN_TIME_WAIT_SECONDS);\n\n        private TimeSpan downTimeWaitTime;\n\n        private bool isDown = true;\n\n        private double locationY = -40.0;\n\n        private bool lanMode;\n\n        public EventHandler LogoutEvent;\n\n        public void AddPrimarySwitchable(ISwitchable switchable)\n        {\n            primarySwitches.Add(switchable);\n            btnMainButton.Text = switchable.GetSwitchName() + \" (F2)\";\n        }\n\n        public void RemovePrimarySwitchable(ISwitchable switchable)\n        {\n            primarySwitches.Remove(switchable);\n            btnMainButton.Text = primarySwitches[primarySwitches.Count - 1].GetSwitchName() + \" (F2)\";\n        }\n\n        public void SetSecondarySwitch(ISwitchable switchable)\n            => cncnetLobbySwitch = switchable;\n\n        public void SetTertiarySwitch(ISwitchable switchable)\n            => privateMessageSwitch = switchable;\n\n        public void SetOptionsWindow(OptionsWindow optionsWindow)\n        {\n            this.optionsWindow = optionsWindow;\n            optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged;\n        }\n\n        private void OptionsWindow_EnabledChanged(object sender, EventArgs e)\n        {\n            if (!lanMode)\n                SetSwitchButtonsClickable(!optionsWindow.Enabled);\n\n            SetOptionsButtonClickable(!optionsWindow.Enabled);\n\n            if (optionsWindow != null)\n                optionsWindow.ToggleMainMenuOnlyOptions(primarySwitches.Count == 1 && !lanMode);\n        }\n\n        public void Clean()\n        {\n            if (cncnetPlayerCountCancellationSource != null)\n                cncnetPlayerCountCancellationSource.Cancel();\n        }\n\n        public override void Initialize()\n        {\n            Name = \"TopBar\";\n            ClientRectangle = new Rectangle(0, -39, WindowManager.RenderResolutionX, 39);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            BackgroundTexture = AssetLoader.CreateTexture(Color.Black, 1, 1);\n            DrawBorders = false;\n\n            btnMainButton = new XNAClientButton(WindowManager);\n            btnMainButton.Name = nameof(btnMainButton);\n            btnMainButton.ClientRectangle = new Rectangle(12, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnMainButton.Text = \"Main Menu (F2)\".L10N(\"Client:Main:MainMenuF2\");\n            btnMainButton.LeftClick += BtnMainButton_LeftClick;\n\n            btnCnCNetLobby = new XNAClientButton(WindowManager);\n            btnCnCNetLobby.Name = nameof(btnCnCNetLobby);\n            btnCnCNetLobby.ClientRectangle = new Rectangle(184, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnCnCNetLobby.Text = \"CnCNet Lobby (F3)\".L10N(\"Client:Main:LobbyF3\");\n            btnCnCNetLobby.LeftClick += BtnCnCNetLobby_LeftClick;\n\n            btnPrivateMessages = new XNAClientButton(WindowManager);\n            btnPrivateMessages.Name = nameof(btnPrivateMessages);\n            btnPrivateMessages.ClientRectangle = new Rectangle(356, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT);\n            btnPrivateMessages.Text = DEFAULT_PM_BTN_LABEL;\n            btnPrivateMessages.LeftClick += BtnPrivateMessages_LeftClick;\n\n            lblDate = new XNALabel(WindowManager);\n            lblDate.Name = nameof(lblDate);\n            lblDate.FontIndex = 1;\n            lblDate.Text = Renderer.GetSafeString(DateTime.Now.ToShortDateString(), lblDate.FontIndex);\n            lblDate.ClientRectangle = new Rectangle(Width -\n                (int)Renderer.GetTextDimensions(lblDate.Text, lblDate.FontIndex).X - 12, 18,\n                lblDate.Width, lblDate.Height);\n\n            lblTime = new XNALabel(WindowManager);\n            lblTime.Name = nameof(lblTime);\n            lblTime.FontIndex = 1;\n            lblTime.Text = Renderer.GetSafeString(new DateTime(1, 1, 1, 23, 59, 59).ToLongTimeString(), lblTime.FontIndex);\n            lblTime.ClientRectangle = new Rectangle(Width -\n                (int)Renderer.GetTextDimensions(lblTime.Text, lblTime.FontIndex).X - 12, 4,\n                lblTime.Width, lblTime.Height);\n\n            btnLogout = new XNAClientButton(WindowManager);\n            btnLogout.Name = nameof(btnLogout);\n            btnLogout.ClientRectangle = new Rectangle(lblDate.X - 87, 9, 75, 23);\n            btnLogout.FontIndex = 1;\n            btnLogout.Text = \"Log Out\".L10N(\"Client:Main:TopBarLogOut\");\n            btnLogout.AllowClick = false;\n            btnLogout.LeftClick += BtnLogout_LeftClick;\n\n            btnOptions = new XNAClientButton(WindowManager);\n            btnOptions.Name = nameof(btnOptions);\n            btnOptions.ClientRectangle = new Rectangle(btnLogout.X - 122, 9, 110, 23);\n            btnOptions.Text = \"Options (F12)\".L10N(\"Client:Main:OptionsF12\");\n            btnOptions.LeftClick += BtnOptions_LeftClick;\n\n            lblConnectionStatus = new XNALabel(WindowManager);\n            lblConnectionStatus.Name = \"lblConnectionStatus\";\n            lblConnectionStatus.FontIndex = 1;\n            lblConnectionStatus.Text = \"OFFLINE\".L10N(\"Client:Main:StatusOffline\");\n\n            AddChild(btnMainButton);\n            AddChild(btnCnCNetLobby);\n            AddChild(btnPrivateMessages);\n            AddChild(btnOptions);\n            AddChild(lblTime);\n            AddChild(lblDate);\n            AddChild(btnLogout);\n            AddChild(lblConnectionStatus);\n\n            if (ClientConfiguration.Instance.DisplayPlayerCountInTopBar)\n            {\n                lblCnCNetStatus = new XNALabel(WindowManager);\n                lblCnCNetStatus.Name = \"lblCnCNetStatus\";\n                lblCnCNetStatus.FontIndex = 1;\n                lblCnCNetStatus.Text = ClientConfiguration.Instance.LocalGame.ToUpper() + \" \" + \"PLAYERS ONLINE:\".L10N(\"Client:Main:OnlinePlayersNumber\");\n                lblCnCNetPlayerCount = new XNALabel(WindowManager);\n                lblCnCNetPlayerCount.Name = \"lblCnCNetPlayerCount\";\n                lblCnCNetPlayerCount.FontIndex = 1;\n                lblCnCNetPlayerCount.Text = \"-\";\n                lblCnCNetPlayerCount.ClientRectangle = new Rectangle(btnOptions.X - 50, 11, lblCnCNetPlayerCount.Width, lblCnCNetPlayerCount.Height);\n                lblCnCNetStatus.ClientRectangle = new Rectangle(lblCnCNetPlayerCount.X - lblCnCNetStatus.Width - 6, 11, lblCnCNetStatus.Width, lblCnCNetStatus.Height);\n                AddChild(lblCnCNetStatus);\n                AddChild(lblCnCNetPlayerCount);\n                CnCNetPlayerCountTask.CnCNetGameCountUpdated += CnCNetInfoController_CnCNetGameCountUpdated;\n                cncnetPlayerCountCancellationSource = new CancellationTokenSource();\n                CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource);\n            }\n\n            lblConnectionStatus.CenterOnParent();\n\n            base.Initialize();\n\n            Keyboard.OnKeyPressed += Keyboard_OnKeyPressed;\n            connectionManager.Connected += ConnectionManager_Connected;\n            connectionManager.Disconnected += ConnectionManager_Disconnected;\n            connectionManager.ConnectionLost += ConnectionManager_ConnectionLost;\n            connectionManager.WelcomeMessageReceived += ConnectionManager_WelcomeMessageReceived;\n            connectionManager.AttemptedServerChanged += ConnectionManager_AttemptedServerChanged;\n            connectionManager.ConnectAttemptFailed += ConnectionManager_ConnectAttemptFailed;\n\n            privateMessageHandler.UnreadMessageCountUpdated += PrivateMessageHandler_UnreadMessageCountUpdated;\n        }\n\n        private void PrivateMessageHandler_UnreadMessageCountUpdated(object sender, UnreadMessageCountEventArgs args)\n            => UpdatePrivateMessagesBtnLabel(args.UnreadMessageCount);\n\n        private void UpdatePrivateMessagesBtnLabel(int unreadMessageCount)\n        {\n            btnPrivateMessages.Text = DEFAULT_PM_BTN_LABEL;\n            if (unreadMessageCount > 0)\n            {\n                // TODO need to make a wider button to accommodate count\n                // btnPrivateMessages.Text += $\" ({unreadMessageCount})\";\n            }\n        }\n\n        private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCountEventArgs e)\n        {\n            lock (locker)\n            {\n                if (e.PlayerCount == -1)\n                    lblCnCNetPlayerCount.Text = \"N/A\".L10N(\"Client:Main:N/A\");\n                else\n                    lblCnCNetPlayerCount.Text = e.PlayerCount.ToString();\n            }\n        }\n\n        private void ConnectionManager_ConnectionLost(object sender, Online.EventArguments.ConnectionLostEventArgs e)\n        {\n            if (!lanMode)\n                ConnectionEvent(\"OFFLINE\".L10N(\"Client:Main:StatusOffline\"));\n        }\n\n        private void ConnectionManager_ConnectAttemptFailed(object sender, EventArgs e)\n        {\n            if (!lanMode)\n                ConnectionEvent(\"OFFLINE\".L10N(\"Client:Main:StatusOffline\"));\n        }\n\n        private void ConnectionManager_AttemptedServerChanged(object sender, Online.EventArguments.AttemptedServerEventArgs e)\n        {\n            ConnectionEvent(\"CONNECTING...\".L10N(\"Client:Main:StatusConnecting\"));\n            BringDown();\n        }\n\n        private void ConnectionManager_WelcomeMessageReceived(object sender, Online.EventArguments.ServerMessageEventArgs e)\n            => ConnectionEvent(\"CONNECTED\".L10N(\"Client:Main:StatusConnected\"));\n\n        private void ConnectionManager_Disconnected(object sender, EventArgs e)\n        {\n            btnLogout.AllowClick = false;\n            if (!lanMode)\n                ConnectionEvent(\"OFFLINE\".L10N(\"Client:Main:StatusOffline\"));\n        }\n\n        private void ConnectionEvent(string text)\n        {\n            lblConnectionStatus.Text = text;\n            lblConnectionStatus.CenterOnParent();\n            isDown = true;\n            downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - EVENT_DOWN_TIME_WAIT_SECONDS);\n        }\n\n        private void BtnLogout_LeftClick(object sender, EventArgs e)\n        {\n            connectionManager.Disconnect();\n            LogoutEvent?.Invoke(this, null);\n            SwitchToPrimary();\n        }\n\n        private void ConnectionManager_Connected(object sender, EventArgs e)\n            => btnLogout.AllowClick = true;\n\n        public void SwitchToPrimary()\n            => BtnMainButton_LeftClick(this, EventArgs.Empty);\n\n        public ISwitchable GetTopMostPrimarySwitchable()\n            => primarySwitches[primarySwitches.Count - 1];\n\n        public void SwitchToSecondary()\n            => BtnCnCNetLobby_LeftClick(this, EventArgs.Empty);\n\n        private void BtnCnCNetLobby_LeftClick(object sender, EventArgs e)\n        {\n            LastSwitchType = SwitchType.SECONDARY;\n            primarySwitches[primarySwitches.Count - 1].SwitchOff();\n            cncnetLobbySwitch.SwitchOn();\n            privateMessageSwitch.SwitchOff();\n\n            // HACK warning\n            // TODO: add a way for DarkeningPanel to skip transitions\n            ((DarkeningPanel)((XNAControl)cncnetLobbySwitch).Parent).Alpha = 1.0f;\n        }\n\n        private void BtnMainButton_LeftClick(object sender, EventArgs e)\n        {\n            LastSwitchType = SwitchType.PRIMARY;\n            cncnetLobbySwitch.SwitchOff();\n            privateMessageSwitch.SwitchOff();\n            primarySwitches[primarySwitches.Count - 1].SwitchOn();\n\n            // HACK warning\n            // TODO: add a way for DarkeningPanel to skip transitions\n            if (((XNAControl)primarySwitches[primarySwitches.Count - 1]).Parent is DarkeningPanel darkeningPanel)\n                darkeningPanel.Alpha = 1.0f;\n        }\n\n        private void BtnPrivateMessages_LeftClick(object sender, EventArgs e)\n            => privateMessageSwitch.SwitchOn();\n\n        private void BtnOptions_LeftClick(object sender, EventArgs e)\n        {\n            privateMessageSwitch.SwitchOff();\n            optionsWindow.Open();\n        }\n\n        private void Keyboard_OnKeyPressed(object sender, KeyPressEventArgs e)\n        {\n            if (!Enabled || !WindowManager.HasFocus || ProgramConstants.IsInGame)\n                return;\n\n            switch (e.PressedKey)\n            {\n                case Keys.F1:\n                    BringDown();\n                    break;\n                case Keys.F2 when btnMainButton.AllowClick:\n                    BtnMainButton_LeftClick(this, EventArgs.Empty);\n                    break;\n                case Keys.F3 when btnCnCNetLobby.AllowClick:\n                    BtnCnCNetLobby_LeftClick(this, EventArgs.Empty);\n                    break;\n                case Keys.F4 when btnPrivateMessages.AllowClick:\n                    BtnPrivateMessages_LeftClick(this, EventArgs.Empty);\n                    break;\n                case Keys.F12 when btnOptions.AllowClick:\n                    BtnOptions_LeftClick(this, EventArgs.Empty);\n                    break;\n            }\n        }\n\n        public override void OnMouseOnControl()\n        {\n            if (Cursor.Location.Y > -1 && !ProgramConstants.IsInGame)\n                BringDown();\n\n            base.OnMouseOnControl();\n        }\n\n        void BringDown()\n        {\n            isDown = true;\n            downTime = TimeSpan.Zero;\n        }\n\n        public void SetMainButtonText(string text)\n            => btnMainButton.Text = text;\n\n        public void SetSwitchButtonsClickable(bool allowClick)\n        {\n            if (btnMainButton != null)\n                btnMainButton.AllowClick = allowClick;\n            if (btnCnCNetLobby != null)\n                btnCnCNetLobby.AllowClick = allowClick;\n            if (btnPrivateMessages != null)\n                btnPrivateMessages.AllowClick = allowClick;\n        }\n\n        public void SetOptionsButtonClickable(bool allowClick)\n        {\n            if (btnOptions != null)\n                btnOptions.AllowClick = allowClick;\n        }\n\n        public void SetLanMode(bool lanMode)\n        {\n            this.lanMode = lanMode;\n            SetSwitchButtonsClickable(!lanMode);\n            if (lanMode)\n                ConnectionEvent(\"LAN MODE\".L10N(\"Client:Main:StatusLanMode\"));\n            else\n                ConnectionEvent(\"OFFLINE\".L10N(\"Client:Main:StatusOffline\"));\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (Cursor.Location.Y < APPEAR_CURSOR_THRESHOLD_Y && Cursor.Location.Y > -1 && !ProgramConstants.IsInGame)\n                BringDown();\n\n            if (isDown)\n            {\n                if (locationY < 0)\n                {\n                    locationY += DOWN_MOVEMENT_RATE * (gameTime.ElapsedGameTime.TotalMilliseconds / 10.0);\n                    ClientRectangle = new Rectangle(X, (int)locationY,\n                        Width, Height);\n                }\n\n                downTime += gameTime.ElapsedGameTime;\n\n                isDown = downTime < downTimeWaitTime;\n            }\n            else\n            {\n                if (locationY > -Height - 1)\n                {\n                    locationY -= UP_MOVEMENT_RATE * (gameTime.ElapsedGameTime.TotalMilliseconds / 10.0);\n                    ClientRectangle = new Rectangle(X, (int)locationY,\n                        Width, Height);\n                }\n                else\n                    return; // Don't handle input when the cursor is above our game window\n            }\n\n            DateTime dtn = DateTime.Now;\n\n            lblTime.Text = Renderer.GetSafeString(dtn.ToLongTimeString(), lblTime.FontIndex);\n            string dateText = Renderer.GetSafeString(dtn.ToShortDateString(), lblDate.FontIndex);\n            if (lblDate.Text != dateText)\n                lblDate.Text = dateText;\n\n            base.Update(gameTime);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            base.Draw(gameTime);\n\n            Renderer.DrawRectangle(new Rectangle(X, ClientRectangle.Bottom - 2, Width, 1), UISettings.ActiveSettings.PanelBorderColor);\n        }\n    }\n\n    public enum SwitchType\n    {\n        PRIMARY,\n        SECONDARY\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/URLHandler.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    public static class URLHandler\n    {\n        /// <summary>\n        /// Checks whether a URL is safe before opening it, prompting a warning as an XNAMessageBox otherwise.\n        /// </summary>\n        public static void OpenLink(WindowManager wm, string url)\n        {\n            // Determine if the links is trusted\n            bool isTrusted = false;\n            try\n            {\n                string domain = new Uri(url).Host;\n                var trustedDomains = ClientConfiguration.Instance.TrustedDomains.Concat(ClientConfiguration.Instance.AlwaysTrustedDomains);\n                isTrusted = trustedDomains.Contains(domain, StringComparer.InvariantCultureIgnoreCase)\n                    || trustedDomains.Any(trustedDomain => domain.EndsWith(\".\" + trustedDomain, StringComparison.InvariantCultureIgnoreCase));\n            }\n            catch (Exception ex)\n            {\n                isTrusted = false;\n                Logger.Log($\"Error in parsing the URL \\\"{url}\\\": {ex.ToString()}\");\n            }\n\n            if (isTrusted)\n            {\n                ProcessLauncher.StartShellProcess(url);\n                return;\n            }\n\n            // Show the warning if the links is not trusted\n            var msgBox = new XNAMessageBox(wm,\n                \"Open Link Confirmation\".L10N(\"Client:Main:OpenLinkConfirmationTitle\"),\n                \"\"\"\n                    You're about to open a link shared in chat.\n\n                    Please note that this link hasn't been verified,\n                    and CnCNet is not responsible for its content.\n\n                    Would you like to open the following link in your browser?\n                    \"\"\".L10N(\"Client:Main:OpenLinkConfirmationText\")\n                + Environment.NewLine + Environment.NewLine + url,\n                XNAMessageBoxButtons.YesNo);\n            msgBox.YesClickedAction = (msgBox) => ProcessLauncher.StartShellProcess(url);\n            msgBox.Show();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/UpdateQueryWindow.cs",
    "content": "﻿using ClientCore;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// A window that asks the user whether they want to update their game.\n    /// </summary>\n    public class UpdateQueryWindow : XNAWindow\n    {\n        public delegate void UpdateAcceptedEventHandler(object sender, EventArgs e);\n        public event UpdateAcceptedEventHandler UpdateAccepted;\n\n        public delegate void UpdateDeclinedEventHandler(object sender, EventArgs e);\n        public event UpdateDeclinedEventHandler UpdateDeclined;\n\n        public UpdateQueryWindow(WindowManager windowManager) : base(windowManager) { }\n\n        private XNALabel lblDescription;\n        private XNALabel lblUpdateSize;\n\n        private string changelogUrl;\n\n        public override void Initialize()\n        {\n            changelogUrl = ClientConfiguration.Instance.ChangelogURL;\n\n            Name = \"UpdateQueryWindow\";\n            ClientRectangle = new Rectangle(0, 0, 251, 140);\n            BackgroundTexture = AssetLoader.LoadTexture(\"updatequerybg.png\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0);\n            lblDescription.Text = String.Empty;\n            lblDescription.Name = nameof(lblDescription);\n\n            var lblChangelogLink = new XNALinkLabel(WindowManager);\n            lblChangelogLink.ClientRectangle = new Rectangle(12, 50, 0, 0);\n            lblChangelogLink.Text = \"View Changelog\".L10N(\"Client:Main:ViewChangeLog\");\n            lblChangelogLink.IdleColor = Color.Goldenrod;\n            lblChangelogLink.Name = nameof(lblChangelogLink);\n            lblChangelogLink.LeftClick += LblChangelogLink_LeftClick;\n\n            lblUpdateSize = new XNALabel(WindowManager);\n            lblUpdateSize.ClientRectangle = new Rectangle(12, 80, 0, 0);\n            lblUpdateSize.Text = String.Empty;\n            lblUpdateSize.Name = nameof(lblUpdateSize);\n\n            var btnYes = new XNAClientButton(WindowManager);\n            btnYes.ClientRectangle = new Rectangle(12, 110, 75, 23);\n            btnYes.Text = \"Yes\".L10N(\"Client:Main:ButtonYes\");\n            btnYes.LeftClick += BtnYes_LeftClick;\n            btnYes.Name = nameof(btnYes);\n\n            var btnNo = new XNAClientButton(WindowManager);\n            btnNo.ClientRectangle = new Rectangle(164, 110, 75, 23);\n            btnNo.Text = \"No\".L10N(\"Client:Main:ButtonNo\");\n            btnNo.LeftClick += BtnNo_LeftClick;\n            btnNo.Name = nameof(btnNo);\n\n            AddChild(lblDescription);\n            AddChild(lblChangelogLink);\n            AddChild(lblUpdateSize);\n            AddChild(btnYes);\n            AddChild(btnNo);\n\n            base.Initialize();\n\n            CenterOnParent();\n        }\n\n        private void LblChangelogLink_LeftClick(object sender, EventArgs e)\n        {\n            ProcessLauncher.StartShellProcess(changelogUrl);\n        }\n\n        private void BtnYes_LeftClick(object sender, EventArgs e)\n        {\n            UpdateAccepted?.Invoke(this, e);\n        }\n\n        private void BtnNo_LeftClick(object sender, EventArgs e)\n        {\n            UpdateDeclined?.Invoke(this, e);\n        }\n\n        public void SetInfo(string version, int updateSize)\n        {\n            lblDescription.Text = string.Format((\"Version {0} is available for download.\\nDo you wish to install it?\").L10N(\"Client:Main:VersionAvailable\"), version);\n            if (updateSize >= 1000)\n                lblUpdateSize.Text = string.Format(\"The size of the update is {0} MB.\".L10N(\"Client:Main:UpdateSizeMB\"), updateSize / 1000);\n            else\n                lblUpdateSize.Text = string.Format(\"The size of the update is {0} KB.\".L10N(\"Client:Main:UpdateSizeKB\"), updateSize);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Generic/UpdateWindow.cs",
    "content": "﻿using ClientGUI;\nusing DTAClient.Domain;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n#if WINFORMS\nusing System.Runtime.InteropServices;\n#endif\nusing ClientUpdater;\n\nnamespace DTAClient.DXGUI.Generic\n{\n    /// <summary>\n    /// The update window, displaying the update progress to the user.\n    /// </summary>\n    public class UpdateWindow : XNAWindow\n    {\n        public delegate void UpdateCancelEventHandler(object sender, EventArgs e);\n        public event UpdateCancelEventHandler UpdateCancelled;\n\n        public delegate void UpdateCompletedEventHandler(object sender, EventArgs e);\n        public event UpdateCompletedEventHandler UpdateCompleted;\n\n        public delegate void UpdateFailureEventHandler(object sender, UpdateFailureEventArgs e);\n        public event UpdateFailureEventHandler UpdateFailed;\n\n        delegate void UpdateProgressChangedDelegate(string fileName, int filePercentage, int totalPercentage);\n        delegate void FileDownloadCompletedDelegate(string archiveName);\n\n        private const double DOT_TIME = 0.66;\n        private const int MAX_DOTS = 5;\n\n        public UpdateWindow(WindowManager windowManager) : base(windowManager)\n        {\n\n        }\n\n        private XNALabel lblDescription;\n        private XNALabel lblCurrentFileProgressPercentageValue;\n        private XNALabel lblTotalProgressPercentageValue;\n        private XNALabel lblCurrentFile;\n        private XNALabel lblUpdaterStatus;\n\n        private XNAProgressBar prgCurrentFile;\n        private XNAProgressBar prgTotal;\n#if WINFORMS\n        private TaskbarProgress tbp;\n#endif\n\n        private bool isStartingForceUpdate;\n\n        bool infoUpdated = false;\n\n        string currFileName = string.Empty;\n        int currFilePercentage = 0;\n        int totalPercentage = 0;\n        int dotCount = 0;\n        double currentDotTime = 0.0;\n\n        private static readonly object locker = new object();\n\n        public override void Initialize()\n        {\n            Name = \"UpdateWindow\";\n            ClientRectangle = new Rectangle(0, 0, 446, 270);\n            BackgroundTexture = AssetLoader.LoadTexture(\"updaterbg.png\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Text = string.Empty;\n            lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0);\n            lblDescription.Name = nameof(lblDescription);\n\n            var lblCurrentFileProgressPercentage = new XNALabel(WindowManager);\n            lblCurrentFileProgressPercentage.Text = \"Progress percentage of current file:\".L10N(\"Client:Main:CurrentFileProgressPercentage\");\n            lblCurrentFileProgressPercentage.ClientRectangle = new Rectangle(12, 90, 0, 0);\n            lblCurrentFileProgressPercentage.Name = nameof(lblCurrentFileProgressPercentage);\n\n            lblCurrentFileProgressPercentageValue = new XNALabel(WindowManager);\n            lblCurrentFileProgressPercentageValue.Text = \"0%\";\n            lblCurrentFileProgressPercentageValue.ClientRectangle = new Rectangle(409, lblCurrentFileProgressPercentage.Y, 0, 0);\n            lblCurrentFileProgressPercentageValue.Name = nameof(lblCurrentFileProgressPercentageValue);\n\n            prgCurrentFile = new XNAProgressBar(WindowManager);\n            prgCurrentFile.Name = nameof(prgCurrentFile);\n            prgCurrentFile.Maximum = 100;\n            prgCurrentFile.ClientRectangle = new Rectangle(12, 110, 422, 30);\n            //prgCurrentFile.BorderColor = UISettings.WindowBorderColor;\n            prgCurrentFile.SmoothForwardTransition = true;\n            prgCurrentFile.SmoothTransitionRate = 10;\n\n            lblCurrentFile = new XNALabel(WindowManager);\n            lblCurrentFile.Name = nameof(lblCurrentFile);\n            lblCurrentFile.ClientRectangle = new Rectangle(12, 142, 0, 0);\n\n            var lblTotalProgressPercentage = new XNALabel(WindowManager);\n            lblTotalProgressPercentage.Text = \"Total progress percentage:\".L10N(\"Client:Main:TotalProgressPercentage\");\n            lblTotalProgressPercentage.ClientRectangle = new Rectangle(12, 170, 0, 0);\n            lblTotalProgressPercentage.Name = nameof(lblTotalProgressPercentage);\n\n            lblTotalProgressPercentageValue = new XNALabel(WindowManager);\n            lblTotalProgressPercentageValue.Text = \"0%\";\n            lblTotalProgressPercentageValue.ClientRectangle = new Rectangle(409, lblTotalProgressPercentage.Y, 0, 0);\n            lblTotalProgressPercentageValue.Name = nameof(lblTotalProgressPercentageValue);\n\n            prgTotal = new XNAProgressBar(WindowManager);\n            prgTotal.Name = nameof(prgTotal);\n            prgTotal.Maximum = 100;\n            prgTotal.ClientRectangle = new Rectangle(12, 190, prgCurrentFile.Width, prgCurrentFile.Height);\n            //prgTotal.BorderColor = UISettings.WindowBorderColor;\n\n            lblUpdaterStatus = new XNALabel(WindowManager);\n            lblUpdaterStatus.Name = nameof(lblUpdaterStatus);\n            lblUpdaterStatus.Text = \"Preparing\".L10N(\"Client:Main:StatusPreparing\");\n            lblUpdaterStatus.ClientRectangle = new Rectangle(12, 240, 0, 0);\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.ClientRectangle = new Rectangle(301, 240, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(lblDescription);\n            AddChild(lblCurrentFileProgressPercentage);\n            AddChild(lblCurrentFileProgressPercentageValue);\n            AddChild(prgCurrentFile);\n            AddChild(lblCurrentFile);\n            AddChild(lblTotalProgressPercentage);\n            AddChild(lblTotalProgressPercentageValue);\n            AddChild(prgTotal);\n            AddChild(lblUpdaterStatus);\n            AddChild(btnCancel);\n\n            base.Initialize(); // Read theme settings from INI\n\n            CenterOnParent();\n\n            Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated;\n            Updater.OnUpdateCompleted += Updater_OnUpdateCompleted;\n            Updater.OnUpdateFailed += Updater_OnUpdateFailed;\n            Updater.UpdateProgressChanged += Updater_UpdateProgressChanged;\n            Updater.LocalFileCheckProgressChanged += Updater_LocalFileCheckProgressChanged;\n            Updater.OnFileDownloadCompleted += Updater_OnFileDownloadCompleted;\n#if WINFORMS\n\n            tbp = new TaskbarProgress();\n#endif\n        }\n\n        private void Updater_FileIdentifiersUpdated()\n        {\n            if (!isStartingForceUpdate)\n                return;\n\n            if (Updater.VersionState == VersionState.UNKNOWN)\n            {\n                XNAMessageBox.Show(WindowManager, \"Force Update Failure\".L10N(\"Client:Main:ForceUpdateFailureTitle\"), \"Checking for updates failed.\".L10N(\"Client:Main:ForceUpdateFailureText\"));\n                AddCallback(new Action(CloseWindow), null);\n                return;\n            }\n            else if (Updater.VersionState == VersionState.OUTDATED && Updater.ManualUpdateRequired)\n            {\n                UpdateCancelled?.Invoke(this, EventArgs.Empty);\n                AddCallback(new Action(CloseWindow), null);\n                return;\n            }\n\n            SetData(Updater.ServerGameVersion);\n            Updater.StartUpdate();\n            isStartingForceUpdate = false;\n        }\n\n        private void Updater_LocalFileCheckProgressChanged(int checkedFileCount, int totalFileCount)\n        {\n            AddCallback(new Action<int>(UpdateFileProgress),\n                (checkedFileCount * 100 / totalFileCount));\n        }\n\n        private void UpdateFileProgress(int value)\n        {\n            prgCurrentFile.Value = value;\n            lblCurrentFileProgressPercentageValue.Text = value + \"%\";\n        }\n\n        private void Updater_UpdateProgressChanged(string currFileName, int currFilePercentage, int totalPercentage)\n        {\n            lock (locker)\n            {\n                infoUpdated = true;\n                this.currFileName = currFileName;\n                this.currFilePercentage = currFilePercentage;\n                this.totalPercentage = totalPercentage;\n            }\n        }\n\n        private void HandleUpdateProgressChange()\n        {\n            if (!infoUpdated)\n                return;\n\n            infoUpdated = false;\n\n            if (currFilePercentage < 0 || currFilePercentage > prgCurrentFile.Maximum)\n                prgCurrentFile.Value = 0;\n            else\n                prgCurrentFile.Value = currFilePercentage;\n\n            if (totalPercentage < 0 || totalPercentage > prgTotal.Maximum)\n                prgTotal.Value = 0;\n            else\n                prgTotal.Value = totalPercentage;\n\n            lblCurrentFileProgressPercentageValue.Text = prgCurrentFile.Value.ToString() + \"%\";\n            lblTotalProgressPercentageValue.Text = prgTotal.Value.ToString() + \"%\";\n            lblCurrentFile.Text = \"Current file:\".L10N(\"Client:Main:CurrentFile\") + \" \" + currFileName;\n            lblUpdaterStatus.Text = \"Downloading files\".L10N(\"Client:Main:DownloadingFiles\");\n#if WINFORMS\n\n            /*/ TODO Improve the updater\n             * When the updater thread in DTAUpdater.dll has completed the update, it will\n             * restart the client right away without giving the UI thread a chance to\n             * finish its tasks and free resources in a proper way.\n             * Because of that, this function is sometimes executed when\n             * the game window has already been hidden / removed, and the code below\n             * will then crash the client, causing the user to see a KABOOM message\n             * along with the successful update, which is likely quite confusing for the user.\n             * The try-catch is a dirty temporary workaround.\n             * /*/\n            try\n            {\n                tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.Normal);\n                tbp.SetValue(WindowManager.GetWindowHandle(), prgTotal.Value, prgTotal.Maximum);\n            }\n            catch\n            {\n            }\n#endif\n        }\n\n        private void Updater_OnFileDownloadCompleted(string archiveName)\n        {\n            AddCallback(new FileDownloadCompletedDelegate(HandleFileDownloadCompleted), archiveName);\n        }\n\n        private void HandleFileDownloadCompleted(string archiveName)\n        {\n            lblUpdaterStatus.Text = \"Unpacking archive\".L10N(\"Client:Main:UnpackingArchive\");\n        }\n\n        private void Updater_OnUpdateCompleted()\n        {\n            AddCallback(new Action(HandleUpdateCompleted), null);\n        }\n\n        private void HandleUpdateCompleted()\n        {\n#if WINFORMS\n            tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress);\n#endif\n            UpdateCompleted?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void Updater_OnUpdateFailed(Exception ex)\n        {\n            AddCallback(new Action<string>(HandleUpdateFailed), ex.Message);\n        }\n\n        private void HandleUpdateFailed(string updateFailureErrorMessage)\n        {\n#if WINFORMS\n            tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress);\n#endif\n            UpdateFailed?.Invoke(this, new UpdateFailureEventArgs(updateFailureErrorMessage));\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            if (!isStartingForceUpdate)\n                Updater.StopUpdate();\n\n            CloseWindow();\n        }\n\n        private void CloseWindow()\n        {\n            isStartingForceUpdate = false;\n\n#if WINFORMS\n            tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress);\n#endif\n            UpdateCancelled?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void SetData(string newGameVersion)\n        {\n            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);\n            lblUpdaterStatus.Text = \"Preparing\".L10N(\"Client:Main:StatusPreparing\");\n        }\n\n        public void ForceUpdate()\n        {\n            isStartingForceUpdate = true;\n            lblDescription.Text = string.Format(\"Force updating {0} to latest version...\".L10N(\"Client:Main:ForceUpdateToLatest\"), MainClientConstants.GAME_NAME_SHORT);\n            lblUpdaterStatus.Text = \"Connecting\".L10N(\"Client:Main:UpdateStatusConnecting\");\n            Updater.CheckForUpdates();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            lock (locker)\n            {\n                HandleUpdateProgressChange();\n            }\n\n            currentDotTime += gameTime.ElapsedGameTime.TotalSeconds;\n            if (currentDotTime > DOT_TIME)\n            {\n                currentDotTime = 0.0;\n                dotCount++;\n                if (dotCount > MAX_DOTS)\n                    dotCount = 0;\n            }\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            base.Draw(gameTime);\n\n            float xOffset = 3.0f;\n\n            for (int i = 0; i < dotCount; i++)\n            {\n                var wrect = lblUpdaterStatus.RenderRectangle();\n                Renderer.DrawStringWithShadow(\".\", lblUpdaterStatus.FontIndex,\n                    new Vector2(wrect.Right + xOffset, wrect.Bottom - 15.0f), lblUpdaterStatus.TextColor);\n                xOffset += 3.0f;\n            }\n        }\n    }\n\n    public class UpdateFailureEventArgs : EventArgs\n    {\n        public UpdateFailureEventArgs(string reason)\n        {\n            this.reason = reason;\n        }\n\n        string reason = String.Empty;\n\n        /// <summary>\n        /// The returned error message from the update failure.\n        /// </summary>\n        public string Reason\n        {\n            get { return reason; }\n        }\n    }\n#if WINFORMS\n\n    /// <summary>\n    /// For utilizing the taskbar progress bar introduced in Windows 7:\n    /// http://stackoverflow.com/questions/1295890/windows-7-progress-bar-in-taskbar-in-c\n    /// </summary>\n    public class TaskbarProgress\n    {\n        public enum TaskbarStates\n        {\n            NoProgress = 0,\n            Indeterminate = 0x1,\n            Normal = 0x2,\n            Error = 0x4,\n            Paused = 0x8\n        }\n\n        [ComImportAttribute()]\n        [GuidAttribute(\"ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf\")]\n        [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]\n        private interface ITaskbarList3\n        {\n            // ITaskbarList\n            [PreserveSig]\n            void HrInit();\n            [PreserveSig]\n            void AddTab(IntPtr hwnd);\n            [PreserveSig]\n            void DeleteTab(IntPtr hwnd);\n            [PreserveSig]\n            void ActivateTab(IntPtr hwnd);\n            [PreserveSig]\n            void SetActiveAlt(IntPtr hwnd);\n\n            // ITaskbarList2\n            [PreserveSig]\n            void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);\n\n            // ITaskbarList3\n            [PreserveSig]\n            void SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal);\n            [PreserveSig]\n            void SetProgressState(IntPtr hwnd, TaskbarStates state);\n        }\n\n        [GuidAttribute(\"56FDF344-FD6D-11d0-958A-006097C9A090\")]\n        [ClassInterfaceAttribute(ClassInterfaceType.None)]\n        [ComImportAttribute()]\n        private class TaskbarInstance\n        {\n        }\n\n        private ITaskbarList3 taskbarInstance = (ITaskbarList3)new TaskbarInstance();\n\n        public void SetState(IntPtr windowHandle, TaskbarStates taskbarState)\n        {\n            taskbarInstance.SetProgressState(windowHandle, taskbarState);\n        }\n\n        public void SetValue(IntPtr windowHandle, double progressValue, double progressMax)\n        {\n            taskbarInstance.SetProgressValue(windowHandle, (ulong)progressValue, (ulong)progressMax);\n        }\n    }\n#endif\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/IGameSessionSetting.cs",
    "content": "using DTAClient.Domain.Multiplayer;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.DXGUI;\n\n// TODO split the logic between campaign/mp and clean up\npublic interface IGameSessionSetting\n{\n    /// <summary>Gets the name of this setting.</summary>\n    string Name { get; }\n\n    /// <summary>Indicates whether this setting can affect spawn.ini.</summary>\n    bool AffectsSpawnIni { get; }\n\n    /// <summary>Indicates whether this setting can affect map code.</summary>\n    bool AffectsMapCode { get; }\n\n    /// <summary>Indicates whether this setting in its current state allows the game to be scored.</summary>\n    bool AllowScoring { get; }\n\n    /// <summary>Indicates whether this setting should be broadcast to the lobby.</summary>\n    bool BroadcastToLobby { get; }\n\n    /// <summary>\n    /// Gets or sets the value of this setting.\n    /// For checkboxes: 0 = unchecked/off, 1 = checked/on.\n    /// For dropdowns: the selected index.\n    /// </summary>\n    int Value { get; set; }\n\n    /// <summary>Applies the associated code to the spawn.ini file.</summary>\n    /// <param name=\"spawnIni\">The spawn.ini file.</param>\n    void ApplySpawnIniCode(IniFile spawnIni);\n\n    /// <summary>Applies the associated code to the map INI file.</summary>\n    /// <param name=\"mapIni\">The map INI file.</param>\n    /// <param name=\"gameMode\">Currently selected gamemode, if applicable.</param>\n    void ApplyMapCode(IniFile mapIni, GameMode gameMode);\n}"
  },
  {
    "path": "DXMainClient/DXGUI/IMessageView.cs",
    "content": "﻿using DTAClient.Online;\n\nnamespace DTAClient.DXGUI\n{\n    public interface IMessageView\n    {\n        void AddMessage(ChatMessage message);\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/ISwitchable.cs",
    "content": "﻿namespace DTAClient.DXGUI\n{\n    /// <summary>\n    /// An interface for all switchable windows.\n    /// </summary>\n    public interface ISwitchable\n    {\n        void SwitchOn();\n\n        void SwitchOff();\n\n        string GetSwitchName();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/ChatListBox.cs",
    "content": "﻿using System;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.Online;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// A list box for CnCNet chat. Supports opening links with a double-click,\n    /// and easy adding of IRC messages to the list box.\n    /// </summary>\n    public class ChatListBox : XNAListBox, IMessageView\n    {\n        public ChatListBox(WindowManager windowManager) : base(windowManager)\n        {\n            DoubleLeftClick += ChatListBox_DoubleLeftClick;\n        }\n\n        private void ChatListBox_DoubleLeftClick(object sender, EventArgs e)\n        {\n            if (SelectedIndex < 0 || SelectedIndex >= Items.Count)\n                return;\n\n            // Get the clicked links\n            string[] links = Items[SelectedIndex].Text?.GetLinks();\n\n            if (links == null)\n                return;\n\n            if (links.Length == 0 || links.Length > 1)\n                return;\n\n            string link = links[0];\n            URLHandler.OpenLink(WindowManager, link);\n        }\n\n        public void AddMessage(string message)\n        {\n            AddMessage(new ChatMessage(message));\n        }\n\n        public void AddMessage(string sender, string message, Color color)\n        {\n            AddMessage(new ChatMessage(sender, color, DateTime.Now, message));\n        }\n\n        public void AddMessage(ChatMessage message)\n        {\n            var listBoxItem = new XNAListBoxItem\n            {\n                TextColor = message.Color,\n                Selectable = true,\n                Tag = message\n            };\n\n            if (message.SenderName == null)\n            {\n                listBoxItem.Text = Renderer.GetSafeString(string.Format(\"[{0}] {1}\",\n                    message.DateTime.ToShortTimeString(),\n                    message.Message), FontIndex);\n            }\n            else\n            {\n                listBoxItem.Text = Renderer.GetSafeString(string.Format(\"[{0}] {1}: {2}\",\n                    message.DateTime.ToShortTimeString(), message.SenderName, message.Message), FontIndex);\n            }\n\n            AddItem(listBoxItem);\n\n            if (LastIndex >= Items.Count - 2)\n            {\n                ScrollToBottom();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/ChoiceNotificationBox.cs",
    "content": "﻿using ClientGUI;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.IO;\nusing System.Reflection;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing SixLabors.ImageSharp;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A box that allows users to make a choice,\n    /// top-left of the game window.\n    /// </summary>\n    public class ChoiceNotificationBox : XNAPanel\n    {\n        private const double DOWN_TIME_WAIT_SECONDS = 4.0;\n        private const double DOWN_MOVEMENT_RATE = 2.0;\n        private const double UP_MOVEMENT_RATE = 2.0;\n\n        public ChoiceNotificationBox(WindowManager windowManager) : base(windowManager)\n        {\n            downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS);\n        }\n\n        public Action<ChoiceNotificationBox> AffirmativeClickedAction { get; set; }\n        public Action<ChoiceNotificationBox> NegativeClickedAction { get; set; }\n\n        private XNALabel lblHeader;\n        private XNAPanel gameIconPanel;\n        private XNALabel lblSender;\n        private XNALabel lblChoiceText;\n        private XNAClientButton affirmativeButton;\n        private XNAClientButton negativeButton;\n\n        private TimeSpan downTime = TimeSpan.Zero;\n\n        private TimeSpan downTimeWaitTime;\n\n        private bool isDown = false;\n\n        private const int boxHeight = 101;\n\n        private double locationY = -boxHeight;\n\n        public override void Initialize()\n        {\n            Name = nameof(ChoiceNotificationBox);\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 196), 1, 1);\n            ClientRectangle = new Rectangle(0, -boxHeight, 300, boxHeight);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            lblHeader = new XNALabel(WindowManager);\n            lblHeader.Name = nameof(lblHeader);\n            lblHeader.FontIndex = 1;\n            lblHeader.AnchorPoint = new Vector2(ClientRectangle.Width / 2, 12);\n            lblHeader.TextAnchor = LabelTextAnchorInfo.CENTER;\n            lblHeader.Text = \"MAKE A CHOICE\".L10N(\"Client:Main:MakeAChoice\");\n            AddChild(lblHeader);\n\n            using Stream dtaIconStream = Assembly.GetAssembly(typeof(GameCollection)).GetManifestResourceStream(\"DTAClient.Icons.dtaicon.png\");\n            using var dtaIcon = Image.Load(dtaIconStream);\n\n            gameIconPanel = new XNAPanel(WindowManager);\n            gameIconPanel.Name = nameof(gameIconPanel);\n            gameIconPanel.ClientRectangle = new Rectangle(12, lblHeader.Bottom + 6, 16, 16);\n            gameIconPanel.DrawBorders = false;\n            gameIconPanel.BackgroundTexture = AssetLoader.TextureFromImage(dtaIcon);\n            AddChild(gameIconPanel);\n\n            lblSender = new XNALabel(WindowManager);\n            lblSender.Name = nameof(lblSender);\n            lblSender.FontIndex = 1;\n            lblSender.ClientRectangle = new Rectangle(gameIconPanel.Right + 3, lblHeader.Bottom + 6, 0, 0);\n            lblSender.Text = \"fonger\";\n            AddChild(lblSender);\n\n            lblChoiceText = new XNALabel(WindowManager);\n            lblChoiceText.Name = nameof(lblChoiceText);\n            lblChoiceText.FontIndex = 1;\n            lblChoiceText.ClientRectangle = new Rectangle(12, lblSender.Bottom + 6, 0, 0);\n            lblChoiceText.Text = \"What do you want to do?\".L10N(\"Client:Main:ChoiceWhatDoYouWant\");\n            AddChild(lblChoiceText);\n\n            affirmativeButton = new XNAClientButton(WindowManager);\n            affirmativeButton.ClientRectangle = new Rectangle(ClientRectangle.Left + 8, lblChoiceText.Bottom + 6, 75, 23);\n            affirmativeButton.Name = nameof(affirmativeButton);\n            affirmativeButton.Text = \"Yes\".L10N(\"Client:Main:ButtonYes\");\n            affirmativeButton.LeftClick += AffirmativeButton_LeftClick;\n            AddChild(affirmativeButton);\n\n            negativeButton = new XNAClientButton(WindowManager);\n            negativeButton.ClientRectangle = new Rectangle(ClientRectangle.Width - (75 + 8), lblChoiceText.Bottom + 6, 75, 23);\n            negativeButton.Name = nameof(negativeButton);\n            negativeButton.Text = \"No\".L10N(\"Client:Main:ButtonNo\");\n            negativeButton.LeftClick += NegativeButton_LeftClick;\n            AddChild(negativeButton);\n\n            base.Initialize();\n        }\n\n        // a timeout of zero means the notification will never be automatically dismissed\n        public void Show(\n            string headerText,\n            Texture2D gameIcon,\n            string sender,\n            string choiceText,\n            string affirmativeText,\n            string negativeText,\n            int timeout = 0)\n        {\n            Enable();\n\n            lblHeader.Text = headerText;\n            gameIconPanel.BackgroundTexture = gameIcon;\n            lblSender.Text = sender;\n            lblChoiceText.Text = choiceText;\n            affirmativeButton.Text = affirmativeText;\n            negativeButton.Text = negativeText;\n\n            // use the same clipping logic as the PM notification\n            if (lblChoiceText.Width > Width)\n            {\n                while (lblChoiceText.Width > Width)\n                {\n                    lblChoiceText.Text = lblChoiceText.Text.Remove(lblChoiceText.Text.Length - 1);\n                }\n            }\n\n            downTime = TimeSpan.Zero;\n            isDown = true;\n\n            downTimeWaitTime = TimeSpan.FromSeconds(timeout);\n        }\n\n        public void Hide()\n        {\n            isDown = false;\n            locationY = -Height;\n            ClientRectangle = new Rectangle(X, (int)locationY,\n                Width, Height);\n            Disable();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (isDown)\n            {\n                if (locationY < 0)\n                {\n                    locationY += DOWN_MOVEMENT_RATE;\n                    ClientRectangle = new Rectangle(X, (int)locationY,\n                        Width, Height);\n                }\n\n                if (WindowManager.HasFocus)\n                {\n                    downTime += gameTime.ElapsedGameTime;\n\n                    // only change our \"down\" state if we have a valid timeout\n                    if (downTimeWaitTime != TimeSpan.Zero)\n                    {\n                        isDown = downTime < downTimeWaitTime;\n                    }\n                }\n            }\n            else\n            {\n                if (locationY > -Height)\n                {\n                    locationY -= UP_MOVEMENT_RATE;\n                    ClientRectangle = new Rectangle(X, (int)locationY, Width, Height);\n                }\n                else\n                {\n                    // effectively delete ourselves when we've timed out\n                    WindowManager.RemoveControl(this);\n                }\n            }\n\n            base.Update(gameTime);\n        }\n\n        private void AffirmativeButton_LeftClick(object sender, EventArgs e)\n        {\n            AffirmativeClickedAction?.Invoke(this);\n            WindowManager.RemoveControl(this);\n        }\n\n        private void NegativeButton_LeftClick(object sender, EventArgs e)\n        {\n            NegativeClickedAction?.Invoke(this);\n            WindowManager.RemoveControl(this);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs",
    "content": "using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers;\nusing DTAClient.Online;\nusing DTAClient.Online.EventArguments;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A game lobby for loading saved CnCNet games.\n    /// </summary>\n    public class CnCNetGameLoadingLobby : GameLoadingLobbyBase\n    {\n        private const double GAME_BROADCAST_INTERVAL = 20.0;\n        private const double INITIAL_GAME_BROADCAST_DELAY = 10.0;\n\n        private const string NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND = \"NPRSNT\";\n        private const string GET_READY_CTCP_COMMAND = \"GTRDY\";\n        private const string FILE_HASH_CTCP_COMMAND = \"FHSH\";\n        private const string INVALID_FILE_HASH_CTCP_COMMAND = \"IHSH\";\n        private const string TUNNEL_PING_CTCP_COMMAND = \"TNLPNG\";\n        private const string OPTIONS_CTCP_COMMAND = \"OP\";\n        private const string INVALID_SAVED_GAME_INDEX_CTCP_COMMAND = \"ISGI\";\n        private const string START_GAME_CTCP_COMMAND = \"START\";\n        private const string PLAYER_READY_CTCP_COMMAND = \"READY\";\n        private const string CHANGE_TUNNEL_SERVER_MESSAGE = \"CHTNL\";\n\n        public CnCNetGameLoadingLobby(\n            WindowManager windowManager,\n            TopBar topBar,\n            CnCNetManager connectionManager,\n            TunnelHandler tunnelHandler,\n            MapLoader mapLoader,\n            GameCollection gameCollection,\n            DiscordHandler discordHandler,\n            CnCNetUserData cncnetUserData\n        ) : base(windowManager, discordHandler)\n        {\n            this.connectionManager = connectionManager;\n            this.tunnelHandler = tunnelHandler;\n            this.topBar = topBar;\n            this.gameCollection = gameCollection;\n            this.mapLoader = mapLoader;\n            this.cncnetUserData = cncnetUserData;\n\n            ctcpCommandHandlers = new CommandHandlerBase[]\n            {\n                new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, HandleNotAllPresentNotification),\n                new NoParamCommandHandler(GET_READY_CTCP_COMMAND, HandleGetReadyNotification),\n                new StringCommandHandler(FILE_HASH_CTCP_COMMAND, HandleFileHashCommand),\n                new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, HandleCheaterNotification),\n                new IntCommandHandler(TUNNEL_PING_CTCP_COMMAND, HandleTunnelPing),\n                new StringCommandHandler(OPTIONS_CTCP_COMMAND, HandleOptionsMessage),\n                new NoParamCommandHandler(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, HandleInvalidSaveIndexCommand),\n                new StringCommandHandler(START_GAME_CTCP_COMMAND, HandleStartGameCommand),\n                new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, HandlePlayerReadyRequest),\n                new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage)\n            };\n        }\n\n        private CommandHandlerBase[] ctcpCommandHandlers;\n\n        private CnCNetManager connectionManager;\n\n        private CnCNetUserData cncnetUserData;\n\n        private List<GameMode> gameModes;\n\n        private TunnelHandler tunnelHandler;\n        private readonly MapLoader mapLoader;\n        private TunnelSelectionWindow tunnelSelectionWindow;\n        private XNAClientButton btnChangeTunnel;\n\n        private Channel channel;\n\n        private GameCollection gameCollection;\n\n        private IRCColor chatColor;\n\n        private string hostName;\n\n        private string localGame;\n\n        private string gameFilesHash;\n\n        private XNATimerControl gameBroadcastTimer;\n\n        private bool started;\n\n        private DarkeningPanel dp;\n\n        private TopBar topBar;\n\n        public override void Initialize()\n        {\n            dp = new DarkeningPanel(WindowManager);\n            //WindowManager.AddAndInitializeControl(dp);\n\n            //dp.AddChildWithoutInitialize(this);\n\n            //dp.Alpha = 0.0f;\n            //dp.Hide();\n            localGame = ClientConfiguration.Instance.LocalGame;\n\n            base.Initialize();\n\n            connectionManager.ConnectionLost += ConnectionManager_ConnectionLost;\n            connectionManager.Disconnected += ConnectionManager_Disconnected;\n\n            tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler);\n            tunnelSelectionWindow.Initialize();\n            tunnelSelectionWindow.DrawOrder = 1;\n            tunnelSelectionWindow.UpdateOrder = 1;\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow);\n            tunnelSelectionWindow.CenterOnParent();\n            tunnelSelectionWindow.Disable();\n            tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected;\n\n            btnChangeTunnel = new XNAClientButton(WindowManager);\n            btnChangeTunnel.Name = nameof(btnChangeTunnel);\n            btnChangeTunnel.ClientRectangle = new Rectangle(btnLeaveGame.Right - btnLeaveGame.Width - 145,\n                btnLeaveGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnChangeTunnel.Text = \"Change Tunnel\".L10N(\"Client:Main:ChangeTunnel\");\n            btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick;\n            AddChild(btnChangeTunnel);\n\n            gameBroadcastTimer = new XNATimerControl(WindowManager);\n            gameBroadcastTimer.AutoReset = true;\n            gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL);\n            gameBroadcastTimer.Enabled = false;\n            gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed;\n\n            WindowManager.AddAndInitializeControl(gameBroadcastTimer);\n        }\n\n        public override void Refresh(bool isHost)\n        {\n            base.Refresh(isHost);\n\n            btnChangeTunnel.Visible = isHost;\n            gameBroadcastTimer.Enabled = isHost;\n        }\n\n        private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow(\"Select tunnel server:\".L10N(\"Client:Main:SelectTunnelServer\"));\n\n        private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame();\n\n        private void ConnectionManager_Disconnected(object sender, EventArgs e) => Clear();\n\n        private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => Clear();\n\n        /// <summary>\n        /// Sets up events and information before joining the channel.\n        /// </summary>\n        public void SetUp(bool isHost, CnCNetTunnel tunnel, Channel channel,\n            string hostName)\n        {\n            this.channel = channel;\n            this.hostName = hostName;\n\n            channel.MessageAdded += Channel_MessageAdded;\n            channel.UserAdded += Channel_UserAdded;\n            channel.UserLeft += Channel_UserLeft;\n            channel.UserQuitIRC += Channel_UserQuitIRC;\n            channel.CTCPReceived += Channel_CTCPReceived;\n\n            tunnelHandler.CurrentTunnel = tunnel;\n            tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged;\n\n            started = false;\n\n            Refresh(isHost);\n        }\n\n        private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e)\n        {\n            // TODO Rampastring pls, review and merge that XNAIndicator PR already\n        }\n\n        /// <summary>\n        /// Clears event subscriptions and leaves the channel.\n        /// </summary>\n        public void Clear()\n        {\n            gameBroadcastTimer.Enabled = false;\n\n            if (channel != null)\n            {\n                // TODO leave channel only if we've joined the channel\n                channel.Leave();\n\n                channel.MessageAdded -= Channel_MessageAdded;\n                channel.UserAdded -= Channel_UserAdded;\n                channel.UserLeft -= Channel_UserLeft;\n                channel.UserQuitIRC -= Channel_UserQuitIRC;\n                channel.CTCPReceived -= Channel_CTCPReceived;\n\n                connectionManager.RemoveChannel(channel);\n            }\n\n            if (Enabled)\n            {\n                Enabled = false;\n                Visible = false;\n\n                base.LeaveGame();\n            }\n\n            tunnelHandler.CurrentTunnel = null;\n            tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged;\n\n            topBar.RemovePrimarySwitchable(this);\n        }\n\n        private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e)\n        {\n            foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers)\n            {\n                if (cmdHandler.Handle(e.UserName, e.Message))\n                    return;\n            }\n\n            Logger.Log(\"Unhandled CTCP command: \" + e.Message + \" from \" + e.UserName);\n        }\n\n        /// <summary>\n        /// Called when the local user has joined the game channel.\n        /// </summary>\n        public void OnJoined()\n        {\n            FileHashCalculator fhc = new FileHashCalculator();\n            fhc.CalculateHashes();\n\n            if (IsHost)\n            {\n                connectionManager.SendCustomMessage(new QueuedMessage(\n                    string.Format(\"MODE {0} +klnNs {1} {2}\", channel.ChannelName,\n                    channel.Password, SGPlayers.Count),\n                    QueuedMessageType.SYSTEM_MESSAGE, 50));\n\n                connectionManager.SendCustomMessage(new QueuedMessage(\n                    string.Format(\"TOPIC {0} :{1}\", channel.ChannelName,\n                    ProgramConstants.CNCNET_PROTOCOL_REVISION + \";\" + localGame.ToLower()),\n                    QueuedMessageType.SYSTEM_MESSAGE, 50));\n\n                gameFilesHash = fhc.GetCompleteHash();\n\n                gameBroadcastTimer.Enabled = true;\n                gameBroadcastTimer.Start();\n                gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY));\n            }\n            else\n            {\n                channel.SendCTCPMessage(FILE_HASH_CTCP_COMMAND + \" \" + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10);\n\n                channel.SendCTCPMessage(TUNNEL_PING_CTCP_COMMAND + \" \" + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10);\n\n                if (tunnelHandler.CurrentTunnel.PingInMs < 0)\n                    AddNotice(string.Format(\"{0} - unknown ping to tunnel server.\".L10N(\"Client:Main:PlayerUnknownPing\"), ProgramConstants.PLAYERNAME));\n                else\n                    AddNotice(string.Format(\"{0} - ping to tunnel server: {1} ms\".L10N(\"Client:Main:PlayerPing\"), ProgramConstants.PLAYERNAME, tunnelHandler.CurrentTunnel.PingInMs));\n            }\n\n            topBar.AddPrimarySwitchable(this);\n            topBar.SwitchToPrimary();\n            WindowManager.SelectedControl = tbChatInput;\n            UpdateDiscordPresence(true);\n        }\n\n        private void Channel_UserAdded(object sender, ChannelUserEventArgs e)\n        {\n            PlayerInfo pInfo = new PlayerInfo();\n            pInfo.Name = e.User.IRCUser.Name;\n\n            Players.Add(pInfo);\n\n            sndJoinSound.Play();\n\n            BroadcastOptions();\n            CopyPlayerDataToUI();\n            UpdateDiscordPresence();\n        }\n\n        private void Channel_UserLeft(object sender, UserNameEventArgs e)\n        {\n            RemovePlayer(e.UserName);\n            UpdateDiscordPresence();\n        }\n\n        private void Channel_UserQuitIRC(object sender, UserNameEventArgs e)\n        {\n            RemovePlayer(e.UserName);\n            UpdateDiscordPresence();\n        }\n\n        private void RemovePlayer(string playerName)\n        {\n            int index = Players.FindIndex(p => p.Name == playerName);\n\n            if (index == -1)\n                return;\n\n            sndLeaveSound.Play();\n\n            Players.RemoveAt(index);\n\n            CopyPlayerDataToUI();\n\n            if (!IsHost && playerName == hostName && !ProgramConstants.IsInGame)\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(\n                    Color.Yellow, \"The game host left the game!\".L10N(\"Client:Main:HostLeft\")));\n\n                Clear();\n            }\n        }\n\n        private void Channel_MessageAdded(object sender, IRCMessageEventArgs e)\n        {\n            if (!string.IsNullOrEmpty(e.Message.SenderIdent) &&\n                cncnetUserData.IsIgnored(e.Message.SenderIdent) &&\n                !e.Message.SenderIsAdmin)\n            {\n                lbChatMessages.AddMessage(new ChatMessage(Color.Silver, string.Format(\"Message blocked from - {0}\".L10N(\"Client:Main:PMBlockedFrom\"), e.Message.SenderName)));\n            }\n            else\n            {\n                lbChatMessages.AddMessage(e.Message);\n                sndMessageSound.Play();\n            }\n        }\n\n        protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message));\n\n        protected override void BroadcastOptions()\n        {\n            if (!IsHost)\n                return;\n\n            //if (Players.Count > 0)\n            Players[0].Ready = true;\n\n            StringBuilder message = new StringBuilder(OPTIONS_CTCP_COMMAND + \" \");\n            message.Append(ddSavedGame.SelectedIndex);\n            message.Append(\";\");\n            foreach (PlayerInfo pInfo in Players)\n            {\n                message.Append(pInfo.Name);\n                message.Append(\":\");\n                message.Append(Convert.ToInt32(pInfo.Ready));\n                message.Append(\";\");\n            }\n            message.Remove(message.Length - 1, 1);\n\n            channel.SendCTCPMessage(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10);\n        }\n\n        protected override void SendChatMessage(string message)\n        {\n            sndMessageSound.Play();\n\n            channel.SendChatMessage(message, chatColor);\n        }\n\n        protected override void RequestReadyStatus() =>\n            channel.SendCTCPMessage(PLAYER_READY_CTCP_COMMAND + \" 1\", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10);\n\n        protected override void GetReadyNotification()\n        {\n            base.GetReadyNotification();\n\n            topBar.SwitchToPrimary();\n\n            if (IsHost)\n                channel.SendCTCPMessage(GET_READY_CTCP_COMMAND, QueuedMessageType.GAME_GET_READY_MESSAGE, 0);\n        }\n\n        protected override void NotAllPresentNotification()\n        {\n            base.NotAllPresentNotification();\n\n            if (IsHost)\n            {\n                channel.SendCTCPMessage(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND,\n                    QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n            }\n        }\n\n        private void ShowTunnelSelectionWindow(string description)\n        {\n            tunnelSelectionWindow.Open(description,\n                tunnelHandler.CurrentTunnel?.Address);\n        }\n\n        private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e)\n        {\n            channel.SendCTCPMessage($\"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}\",\n                QueuedMessageType.SYSTEM_MESSAGE, 10);\n            HandleTunnelServerChange(e.Tunnel);\n        }\n\n        #region CTCP Handlers\n\n        private void HandleGetReadyNotification(string sender)\n        {\n            if (sender != hostName)\n                return;\n\n            GetReadyNotification();\n        }\n\n        private void HandleNotAllPresentNotification(string sender)\n        {\n            if (sender != hostName)\n                return;\n\n            NotAllPresentNotification();\n        }\n\n        private void HandleFileHashCommand(string sender, string fileHash)\n        {\n            if (!IsHost)\n                return;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n            if (pInfo == null)\n                return;\n\n            pInfo.HashReceived = true;\n\n            if (fileHash != gameFilesHash)\n                HandleCheaterNotification(hostName, sender); // This is kinda hacky\n        }\n\n        private void HandleCheaterNotification(string sender, string cheaterName)\n        {\n            if (sender != hostName)\n                return;\n\n            AddNotice(string.Format(\"{0} - modified files detected! They could be cheating!\".L10N(\"Client:Main:PlayerCheating\"), cheaterName), Color.Red);\n\n            if (IsHost)\n                channel.SendCTCPMessage(INVALID_FILE_HASH_CTCP_COMMAND + \" \" + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0);\n        }\n\n        private void HandleTunnelPing(string sender, int pingInMs)\n        {\n            if (pingInMs < 0)\n                AddNotice(string.Format(\"{0} - unknown ping to tunnel server.\".L10N(\"Client:Main:PlayerUnknownPing\"), sender));\n            else\n                AddNotice(string.Format(\"{0} - ping to tunnel server: {1} ms\".L10N(\"Client:Main:PlayerPing\"), sender, pingInMs));\n        }\n\n        /// <summary>\n        /// Handles an options broadcast sent by the game host.\n        /// </summary>\n        private void HandleOptionsMessage(string sender, string data)\n        {\n            if (sender != hostName)\n                return;\n\n            string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);\n\n            if (parts.Length < 1)\n                return;\n\n            int sgIndex = Conversions.IntFromString(parts[0], -1);\n\n            if (sgIndex < 0)\n                return;\n\n            if (sgIndex >= ddSavedGame.Items.Count)\n            {\n                AddNotice(\"The game host has selected an invalid saved game index!\".L10N(\"Client:Main:HostInvalidIndex\") + \" \" + sgIndex);\n                channel.SendCTCPMessage(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10);\n                return;\n            }\n\n            ddSavedGame.SelectedIndex = sgIndex;\n\n            Players.Clear();\n\n            for (int i = 1; i < parts.Length; i++)\n            {\n                string[] playerAndReadyStatus = parts[i].Split(':');\n                if (playerAndReadyStatus.Length < 2)\n                    return;\n\n                string playerName = playerAndReadyStatus[0];\n                int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1);\n\n                if (string.IsNullOrEmpty(playerName) || readyStatus == -1)\n                    return;\n\n                PlayerInfo pInfo = new PlayerInfo();\n                pInfo.Name = playerName;\n                pInfo.Ready = Convert.ToBoolean(readyStatus);\n\n                Players.Add(pInfo);\n            }\n\n            CopyPlayerDataToUI();\n        }\n\n        private void HandleInvalidSaveIndexCommand(string sender)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo == null)\n                return;\n\n            pInfo.Ready = false;\n\n            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));\n\n            CopyPlayerDataToUI();\n        }\n\n        private void HandleStartGameCommand(string sender, string data)\n        {\n            if (sender != hostName)\n                return;\n\n            string[] parts = data.Split(';');\n\n            int playerCount = parts.Length / 2;\n\n            for (int i = 0; i < playerCount; i++)\n            {\n                if (parts.Length < i * 2 + 1)\n                    return;\n\n                string pName = parts[i * 2];\n                string ipAndPort = parts[i * 2 + 1];\n                string[] ipAndPortSplit = ipAndPort.Split(':');\n\n                if (ipAndPortSplit.Length < 2)\n                    return;\n\n                int port = 0;\n                bool success = int.TryParse(ipAndPortSplit[1], out port);\n                if (!success)\n                    return;\n\n                PlayerInfo pInfo = Players.Find(p => p.Name == pName);\n\n                if (pInfo == null)\n                    continue;\n\n                pInfo.Port = port;\n            }\n\n            LoadGame();\n        }\n\n        private void HandlePlayerReadyRequest(string sender, int readyStatus)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo == null)\n                return;\n\n            pInfo.Ready = Convert.ToBoolean(readyStatus);\n\n            CopyPlayerDataToUI();\n\n            if (IsHost)\n                BroadcastOptions();\n        }\n\n        private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort)\n        {\n            if (sender != hostName)\n                return;\n\n            string[] split = tunnelAddressAndPort.Split(':');\n            string tunnelAddress = split[0];\n            int tunnelPort = int.Parse(split[1]);\n\n            CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort);\n            if (tunnel == null)\n            {\n                AddNotice((\"The game host has selected an invalid tunnel server! \" +\n                    \"The game host needs to change the server or you will be unable \" +\n                    \"to participate in the match.\").L10N(\"Client:Main:HostInvalidTunnel\"),\n                    Color.Yellow);\n                btnLoadGame.AllowClick = false;\n                return;\n            }\n\n            HandleTunnelServerChange(tunnel);\n            btnLoadGame.AllowClick = true;\n        }\n\n        /// <summary>\n        /// Changes the tunnel server used for the game.\n        /// </summary>\n        /// <param name=\"tunnel\">The new tunnel server to use.</param>\n        private void HandleTunnelServerChange(CnCNetTunnel tunnel)\n        {\n            tunnelHandler.CurrentTunnel = tunnel;\n            AddNotice(string.Format(\"The game host has changed the tunnel server to: {0}\".L10N(\"Client:Main:HostChangeTunnel\"), tunnel.Name));\n            //UpdatePing();\n        }\n\n        #endregion\n\n        protected override void HostStartGame()\n        {\n            AddNotice(\"Contacting tunnel server...\".L10N(\"Client:Main:ConnectingTunnel\"));\n            List<int> playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(SGPlayers.Count);\n\n            if (playerPorts.Count < Players.Count)\n            {\n                ShowTunnelSelectionWindow((\"An error occured while contacting the CnCNet tunnel server.\\nTry picking a different tunnel server:\").L10N(\"Client:Main:ConnectTunnelError1\"));\n                AddNotice((\"An error occured while contacting the specified CnCNet \" +\n                    \"tunnel server. Please try using a different tunnel server\").L10N(\"Client:Main:ConnectTunnelError2\") + \" \", Color.Yellow);\n                return;\n            }\n\n            StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + \" \");\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                Players[pId].Port = playerPorts[pId];\n                sb.Append(Players[pId].Name);\n                sb.Append(\";\");\n                sb.Append(\"0.0.0.0:\");\n                sb.Append(playerPorts[pId]);\n                sb.Append(\";\");\n            }\n            sb.Remove(sb.Length - 1, 1);\n            channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9);\n\n            AddNotice(\"Starting game...\".L10N(\"Client:Main:StartingGame\"));\n\n            started = true;\n\n            LoadGame();\n        }\n\n        protected override void WriteSpawnIniAdditions(IniFile spawnIni)\n        {\n            spawnIni.SetStringValue(\"Tunnel\", \"Ip\", tunnelHandler.CurrentTunnel.Address);\n            spawnIni.SetIntValue(\"Tunnel\", \"Port\", tunnelHandler.CurrentTunnel.Port);\n\n            base.WriteSpawnIniAdditions(spawnIni);\n        }\n\n        protected override void HandleGameProcessExited()\n        {\n            base.HandleGameProcessExited();\n\n            Clear();\n        }\n\n        protected override void LeaveGame() => Clear();\n\n        public void ChangeChatColor(IRCColor chatColor)\n        {\n            this.chatColor = chatColor;\n            tbChatInput.TextColor = chatColor.XnaColor;\n        }\n\n        private void BroadcastGame()\n        {\n            Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame));\n\n            if (broadcastChannel == null)\n                return;\n\n            StringBuilder sb = new StringBuilder(\"GAME \");\n            sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION);\n            sb.Append(\";\");\n            sb.Append(ProgramConstants.GAME_VERSION);\n            sb.Append(\";\");\n            sb.Append(SGPlayers.Count);\n            sb.Append(\";\");\n            sb.Append(channel.ChannelName);\n            sb.Append(\";\");\n            sb.Append(channel.UIName);\n            sb.Append(\";\");\n            if (started || Players.Count == SGPlayers.Count)\n                sb.Append(\"1\");\n            else\n                sb.Append(\"0\");\n            sb.Append(\"0\"); // IsCustomPassword\n            sb.Append(\"0\"); // Closed\n            sb.Append(\"1\"); // IsLoadedGame\n            sb.Append(\"0\"); // IsLadder\n            sb.Append(\";\");\n            foreach (SavedGamePlayer sgPlayer in SGPlayers)\n            {\n                sb.Append(sgPlayer.Name);\n                sb.Append(\",\");\n            }\n\n            sb.Remove(sb.Length - 1, 1);\n            sb.Append(\";\");\n            sb.Append((string)lblMapNameValue.Tag);\n            sb.Append(\";\");\n            sb.Append((string)lblGameModeValue.Tag);\n            sb.Append(\";\");\n            sb.Append(tunnelHandler.CurrentTunnel.Address + \":\" + tunnelHandler.CurrentTunnel.Port);\n            sb.Append(\";\");\n            sb.Append(0); // LoadedGameId\n            sb.Append(\";\");\n            sb.Append(ClientConfiguration.Instance.DefaultSkillLevelIndex); // we don't know the original skill level\n            sb.Append(\";\"); // Map SHA1\n            sb.Append(\";\"); // Game option values\n\n            broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20);\n        }\n\n        public override string GetSwitchName() => \"Load Game\".L10N(\"Client:Main:LoadGame\");\n\n        protected override void UpdateDiscordPresence(bool resetTimer = false)\n        {\n            if (discordHandler == null)\n                return;\n\n            PlayerInfo player = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n            if (player == null)\n                return;\n            string currentState = ProgramConstants.IsInGame ? \"In Game\" : \"In Lobby\"; // not UI strings\n\n            discordHandler.UpdatePresence(\n                (string)lblMapNameValue.Tag, (string)lblGameModeValue.Tag, \"Multiplayer\",\n                currentState, Players.Count, SGPlayers.Count,\n                channel.UIName, IsHost, resetTimer);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs",
    "content": "﻿using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing DTAClient.Online;\nusing DTAClient.Online.EventArguments;\nusing DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading;\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\nusing SixLabors.ImageSharp;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    using UserChannelPair = Tuple<string, string>;\n    using InvitationIndex = Dictionary<Tuple<string, string>, WeakReference>;\n\n    internal class CnCNetLobby : XNAWindow, ISwitchable\n    {\n        public event EventHandler UpdateCheck;\n\n        public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager,\n            CnCNetGameLobby gameLobby, CnCNetGameLoadingLobby gameLoadingLobby,\n            TopBar topBar, PrivateMessagingWindow pmWindow, TunnelHandler tunnelHandler,\n            GameCollection gameCollection, CnCNetUserData cncnetUserData,\n            OptionsWindow optionsWindow, MapLoader mapLoader, Random random)\n            : base(windowManager)\n        {\n            this.connectionManager = connectionManager;\n            this.gameLobby = gameLobby;\n            this.gameLoadingLobby = gameLoadingLobby;\n            this.tunnelHandler = tunnelHandler;\n            this.topBar = topBar;\n            this.pmWindow = pmWindow;\n            this.gameCollection = gameCollection;\n            this.cncnetUserData = cncnetUserData;\n            this.optionsWindow = optionsWindow;\n            this.mapLoader = mapLoader;\n            this.random = random;\n\n            ctcpCommandHandlers = new CommandHandlerBase[]\n            {\n                new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, HandleGameInviteCommand),\n                new NoParamCommandHandler(ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND, HandleGameInvitationFailedNotification)\n            };\n\n            topBar.LogoutEvent += LogoutEvent;\n        }\n\n        private MapLoader mapLoader;\n\n        private CnCNetManager connectionManager;\n        private CnCNetUserData cncnetUserData;\n        private readonly OptionsWindow optionsWindow;\n\n        private PlayerListBox lbPlayerList;\n        private ChatListBox lbChatMessages;\n        private GameListBox lbGameList;\n        private GlobalContextMenu globalContextMenu;\n\n        private XNAClientButton btnLogout;\n        private XNAClientButton btnNewGame;\n        private XNAClientButton btnJoinGame;\n\n        private XNAChatTextBox tbChatInput;\n\n        private XNALabel lblColor;\n        private XNALabel lblCurrentChannel;\n        private XNALabel lblOnline;\n        private XNALabel lblOnlineCount;\n\n        private XNAClientDropDown ddColor;\n        private XNAClientDropDown ddCurrentChannel;\n\n        private XNASuggestionTextBox tbGameSearch;\n\n        private XNAClientStateButton<SortDirection> btnGameSortAlpha;\n\n        private XNAClientToggleButton btnGameFilterOptions;\n\n        private DarkeningPanel gameCreationPanel;\n\n        private Channel currentChatChannel;\n\n        private GameCollection gameCollection;\n\n        private Color cAdminNameColor;\n\n        private Texture2D unknownGameIcon;\n        private Texture2D adminGameIcon;\n\n        private EnhancedSoundEffect sndGameCreated;\n        private EnhancedSoundEffect sndGameInviteReceived;\n\n        private IRCColor[] chatColors;\n\n        private CnCNetGameLobby gameLobby;\n        private CnCNetGameLoadingLobby gameLoadingLobby;\n\n        private TunnelHandler tunnelHandler;\n\n        private CnCNetLoginWindow loginWindow;\n\n        private TopBar topBar;\n\n        private PrivateMessagingWindow pmWindow;\n\n        private PasswordRequestWindow passwordRequestWindow;\n\n        private bool isInGameRoom = false;\n        private bool updateDenied = false;\n\n        private string localGameID;\n        private CnCNetGame localGame;\n\n        private List<string> followedGames = new List<string>();\n\n        private bool isJoiningGame = false;\n        private HostedCnCNetGame gameOfLastJoinAttempt;\n\n        private CancellationTokenSource gameCheckCancellation;\n\n        private CommandHandlerBase[] ctcpCommandHandlers;\n\n        private InvitationIndex invitationIndex;\n\n        private GameFiltersPanel panelGameFilters;\n\n        private Random random;\n\n        private bool ctcpInvalidGameMessageShown = false;\n        private bool ctcpNoTunnelMessageShown = false;\n        private bool ctcpNoTunnelForGamesMessageShown = false;\n\n        private void GameList_ClientRectangleUpdated(object sender, EventArgs e)\n        {\n            panelGameFilters.ClientRectangle = lbGameList.ClientRectangle;\n        }\n\n        private void LogoutEvent(object sender, EventArgs e)\n        {\n            isJoiningGame = false;\n        }\n\n        public override void Initialize()\n        {\n            invitationIndex = new InvitationIndex();\n\n            ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 64,\n                WindowManager.RenderResolutionY - 64);\n\n            Name = nameof(CnCNetLobby);\n            BackgroundTexture = AssetLoader.LoadTexture(\"cncnetlobbybg.png\");\n            localGameID = ClientConfiguration.Instance.LocalGame;\n            localGame = gameCollection.GameList.Find(g => g.InternalName.ToUpper() == localGameID.ToUpper());\n\n            btnNewGame = new XNAClientButton(WindowManager);\n            btnNewGame.Name = nameof(btnNewGame);\n            btnNewGame.ClientRectangle = new Rectangle(12, Height - 29, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnNewGame.Text = \"Create Game\".L10N(\"Client:Main:CreateGame\");\n            btnNewGame.AllowClick = false;\n            btnNewGame.LeftClick += BtnNewGame_LeftClick;\n\n            btnJoinGame = new XNAClientButton(WindowManager);\n            btnJoinGame.Name = nameof(btnJoinGame);\n            btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12,\n                btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnJoinGame.Text = \"Join Game\".L10N(\"Client:Main:JoinGame\");\n            btnJoinGame.AllowClick = false;\n            btnJoinGame.LeftClick += BtnJoinGame_LeftClick;\n\n            btnLogout = new XNAClientButton(WindowManager);\n            btnLogout.Name = nameof(btnLogout);\n            btnLogout.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y,\n                UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLogout.Text = \"Log Out\".L10N(\"Client:Main:LogOut\");\n            btnLogout.LeftClick += BtnLogout_LeftClick;\n\n            var gameListRectangle = new Rectangle(\n                btnNewGame.X, 41,\n                btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 47\n            );\n\n            panelGameFilters = new GameFiltersPanel(WindowManager, gameLobby);\n            panelGameFilters.Name = nameof(panelGameFilters);\n            panelGameFilters.ClientRectangle = gameListRectangle;\n            panelGameFilters.Disable();\n\n            lbGameList = new GameListBox(WindowManager, mapLoader, localGameID, gameLobby, HostedGameMatches);\n            lbGameList.Name = nameof(lbGameList);\n            lbGameList.ClientRectangle = gameListRectangle;\n            lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick;\n            lbGameList.RightClick += LbGameList_RightClick;\n            lbGameList.AllowMultiLineItems = false;\n            lbGameList.ClientRectangleUpdated += GameList_ClientRectangleUpdated;\n\n            lbPlayerList = new PlayerListBox(WindowManager, gameCollection);\n            lbPlayerList.Name = nameof(lbPlayerList);\n            lbPlayerList.ClientRectangle = new Rectangle(Width - 202,\n                20, 190,\n                btnLogout.Y - 26);\n            lbPlayerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbPlayerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbPlayerList.LineHeight = 16;\n            lbPlayerList.DoubleLeftClick += LbPlayerList_DoubleLeftClick;\n            lbPlayerList.RightClick += LbPlayerList_RightClick;\n\n            globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow);\n            globalContextMenu.JoinEvent += (sender, args) => JoinUser(args.IrcUser, connectionManager.MainChannel);\n\n            lbChatMessages = new ChatListBox(WindowManager);\n            lbChatMessages.Name = nameof(lbChatMessages);\n            lbChatMessages.ClientRectangle = new Rectangle(lbGameList.Right + 12, lbGameList.Y,\n                lbPlayerList.X - lbGameList.Right - 24, lbPlayerList.Height);\n            lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbChatMessages.LineHeight = 16;\n            lbChatMessages.LeftClick += (sender, args) => lbGameList.SelectedIndex = -1;\n            lbChatMessages.RightClick += LbChatMessages_RightClick;\n\n            tbChatInput = new XNAChatTextBox(WindowManager);\n            tbChatInput.Name = nameof(tbChatInput);\n            tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X,\n                btnNewGame.Y, lbChatMessages.Width,\n                btnNewGame.Height);\n            tbChatInput.Suggestion = \"Type here to chat...\".L10N(\"Client:Main:ChatHere\");\n            tbChatInput.Enabled = false;\n            tbChatInput.MaximumTextLength = 200;\n            tbChatInput.EnterPressed += TbChatInput_EnterPressed;\n\n            lblColor = new XNALabel(WindowManager);\n            lblColor.Name = nameof(lblColor);\n            lblColor.ClientRectangle = new Rectangle(lbChatMessages.X, 14, 0, 0);\n            lblColor.FontIndex = 1;\n            lblColor.Text = \"YOUR COLOR:\".L10N(\"Client:Main:YourColor\");\n\n            ddColor = new XNAClientDropDown(WindowManager);\n            ddColor.Name = nameof(ddColor);\n            ddColor.ClientRectangle = new Rectangle(lblColor.X + 95, 12,\n                150, 21);\n\n            chatColors = connectionManager.GetIRCColors();\n\n            foreach (IRCColor color in connectionManager.GetIRCColors())\n            {\n                if (!color.Selectable)\n                    continue;\n\n                XNADropDownItem ddItem = new XNADropDownItem();\n                ddItem.Text = color.Name;\n                ddItem.TextColor = color.XnaColor;\n                ddItem.Tag = color;\n\n                ddColor.AddItem(ddItem);\n            }\n\n            int selectedColor = UserINISettings.Instance.ChatColor;\n\n            ddColor.SelectedIndex = selectedColor >= ddColor.Items.Count || selectedColor < 0\n                ? ClientConfiguration.Instance.DefaultPersonalChatColorIndex :\n                selectedColor;\n            SetChatColor();\n            ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged;\n\n            ddCurrentChannel = new XNAClientDropDown(WindowManager);\n            ddCurrentChannel.Name = nameof(ddCurrentChannel);\n            ddCurrentChannel.ClientRectangle = new Rectangle(\n                lbChatMessages.Right - 200,\n                ddColor.Y, 200, 21);\n            ddCurrentChannel.SelectedIndexChanged += DdCurrentChannel_SelectedIndexChanged;\n            ddCurrentChannel.AllowDropDown = false;\n\n            lblCurrentChannel = new XNALabel(WindowManager);\n            lblCurrentChannel.Name = nameof(lblCurrentChannel);\n            lblCurrentChannel.ClientRectangle = new Rectangle(\n                ddCurrentChannel.X - 150,\n                ddCurrentChannel.Y + 2, 0, 0);\n            lblCurrentChannel.FontIndex = 1;\n            lblCurrentChannel.Text = \"CURRENT CHANNEL:\".L10N(\"Client:Main:CurrentChannel\");\n\n            lblOnline = new XNALabel(WindowManager);\n            lblOnline.Name = nameof(lblOnline);\n            lblOnline.ClientRectangle = new Rectangle(310, 14, 0, 0);\n            lblOnline.Text = \"Online:\".L10N(\"Client:Main:OnlineLabel\");\n            lblOnline.FontIndex = 1;\n            lblOnline.Disable();\n\n            lblOnlineCount = new XNALabel(WindowManager);\n            lblOnlineCount.Name = nameof(lblOnlineCount);\n            lblOnlineCount.ClientRectangle = new Rectangle(lblOnline.X + 50, 14, 0, 0);\n            lblOnlineCount.FontIndex = 1;\n            lblOnlineCount.Disable();\n\n            tbGameSearch = new XNASuggestionTextBox(WindowManager);\n            tbGameSearch.Name = nameof(tbGameSearch);\n            tbGameSearch.ClientRectangle = new Rectangle(lbGameList.X,\n                12, lbGameList.Width - 62, 21);\n            tbGameSearch.Suggestion = \"Filter by name, map, game mode, player...\".L10N(\"Client:Main:FilterByBlahBlah\");\n            tbGameSearch.MaximumTextLength = 64;\n            tbGameSearch.InputReceived += TbGameSearch_InputReceived;\n            tbGameSearch.Disable();\n\n            btnGameSortAlpha = new XNAClientStateButton<SortDirection>(WindowManager, new Dictionary<SortDirection, Texture2D>()\n            {\n                { SortDirection.None, AssetLoader.LoadTexture(\"sortAlphaNone.png\") },\n                { SortDirection.Asc, AssetLoader.LoadTexture(\"sortAlphaAsc.png\") },\n                { SortDirection.Desc, AssetLoader.LoadTexture(\"sortAlphaDesc.png\") },\n            });\n            btnGameSortAlpha.Name = nameof(btnGameSortAlpha);\n            btnGameSortAlpha.ClientRectangle = new Rectangle(\n                tbGameSearch.X + tbGameSearch.Width + 10, tbGameSearch.Y,\n                21, 21);\n            btnGameSortAlpha.LeftClick += BtnGameSortAlpha_LeftClick;\n            btnGameSortAlpha.SetToolTipText(\"Sort Games Alphabetically\".L10N(\"Client:Main:SortAlphabet\"));\n            RefreshGameSortAlphaBtn();\n\n            btnGameFilterOptions = new XNAClientToggleButton(WindowManager);\n            btnGameFilterOptions.Name = nameof(btnGameFilterOptions);\n            btnGameFilterOptions.ClientRectangle = new Rectangle(\n                btnGameSortAlpha.X + btnGameSortAlpha.Width + 10, tbGameSearch.Y,\n                21, 21);\n            btnGameFilterOptions.CheckedTexture = AssetLoader.LoadTexture(\"filterActive.png\");\n            btnGameFilterOptions.UncheckedTexture = AssetLoader.LoadTexture(\"filterInactive.png\");\n            btnGameFilterOptions.LeftClick += BtnGameFilterOptions_LeftClick;\n            btnGameFilterOptions.SetToolTipText(\"Game Filters\".L10N(\"Client:Main:GameFilters\"));\n            RefreshGameFiltersBtn();\n\n            InitializeGameList();\n\n            AddChild(btnNewGame);\n            AddChild(btnJoinGame);\n            AddChild(btnLogout);\n            AddChild(lbPlayerList);\n            AddChild(lbChatMessages);\n            AddChild(lbGameList);\n            AddChild(panelGameFilters);\n            AddChild(tbChatInput);\n            AddChild(lblColor);\n            AddChild(ddColor);\n            AddChild(lblCurrentChannel);\n            AddChild(ddCurrentChannel);\n            AddChild(globalContextMenu);\n            AddChild(lblOnline);\n            AddChild(lblOnlineCount);\n            AddChild(tbGameSearch);\n            AddChild(btnGameSortAlpha);\n            AddChild(btnGameFilterOptions);\n\n\n            panelGameFilters.VisibleChanged += GameFiltersPanel_VisibleChanged;\n\n            CnCNetPlayerCountTask.CnCNetGameCountUpdated += OnCnCNetGameCountUpdated;\n            UpdateOnlineCount(CnCNetPlayerCountTask.PlayerCount);\n\n            pmWindow.SetJoinUserAction(JoinUser);\n\n            base.Initialize();\n\n            WindowManager.CenterControlOnScreen(this);\n\n            PostUIInit();\n        }\n\n        private void BtnGameSortAlpha_LeftClick(object sender, EventArgs e)\n        {\n            UserINISettings.Instance.SortState.Value = (int)btnGameSortAlpha.GetState();\n\n            RefreshGameSortAlphaBtn();\n            SortAndRefreshHostedGames();\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        private void SortAndRefreshHostedGames()\n        {\n            lbGameList.SortAndRefreshHostedGames();\n        }\n\n        private void BtnGameFilterOptions_LeftClick(object sender, EventArgs e)\n        {\n            if (panelGameFilters.Visible)\n                panelGameFilters.Cancel();\n            else\n                panelGameFilters.Show();\n        }\n\n        private void RefreshGameSortAlphaBtn()\n        {\n            if (Enum.IsDefined(typeof(SortDirection), UserINISettings.Instance.SortState.Value))\n                btnGameSortAlpha.SetState((SortDirection)UserINISettings.Instance.SortState.Value);\n        }\n\n        private void RefreshGameFiltersBtn()\n        {\n            btnGameFilterOptions.Checked = UserINISettings.Instance.IsGameFiltersApplied();\n        }\n\n        private void GameFiltersPanel_VisibleChanged(object sender, EventArgs e)\n        {\n            if (panelGameFilters.Visible)\n                return;\n\n            RefreshGameFiltersBtn();\n            SortAndRefreshHostedGames();\n        }\n\n        private void TbGameSearch_InputReceived(object sender, EventArgs e)\n        {\n            SortAndRefreshHostedGames();\n            lbGameList.ViewTop = 0;\n        }\n\n\n        /// <summary>\n        /// Checks if a hosted game matches the current filter criteria.\n        /// </summary>\n        /// <param name=\"hg\">The hosted game to check.</param>\n        /// <returns>True if the game matches the filter criteria, false otherwise.</returns>\n        private bool HostedGameMatches(GenericHostedGame hg)\n        {\n            // friends list takes priority over other filters below\n            if (UserINISettings.Instance.ShowFriendGamesOnly)\n                return hg.Players.Any(cncnetUserData.IsFriend);\n\n            if (UserINISettings.Instance.HideLockedGames.Value && hg.Locked)\n                return false;\n\n            if (UserINISettings.Instance.HideIncompatibleGames.Value && hg.Incompatible)\n                return false;\n\n            if (UserINISettings.Instance.HidePasswordedGames.Value && hg.Passworded)\n                return false;\n\n            if (hg.MaxPlayers > UserINISettings.Instance.MaxPlayerCount.Value)\n                return false;\n\n            if (hg is HostedCnCNetGame cncnetGame && !GameOptionsMatch(cncnetGame))\n                return false;\n\n            string textUpper = tbGameSearch?.Text?.ToUpperInvariant();\n\n            string translatedGameMode = string.IsNullOrEmpty(hg.GameMode)\n                ? \"Unknown\".L10N(\"Client:Main:Unknown\")\n                : hg.GameMode.L10N($\"INI:GameModes:{hg.GameMode}:UIName\", notify: false);\n\n            string translatedMapName = string.IsNullOrEmpty(hg.Map)\n                ? \"Unknown\".L10N(\"Client:Main:Unknown\") : mapLoader.TranslatedMapNames.ContainsKey(hg.Map)\n                ? mapLoader.TranslatedMapNames[hg.Map] : null;\n\n            return\n                string.IsNullOrWhiteSpace(tbGameSearch?.Text) ||\n                tbGameSearch.Text == tbGameSearch.Suggestion ||\n                hg.RoomName.ToUpperInvariant().Contains(textUpper) ||\n                hg.GameMode.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal) ||\n                translatedGameMode.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal) ||\n                hg.Map.ToUpperInvariant().Contains(textUpper) ||\n                (translatedMapName is not null && translatedMapName.ToUpperInvariant().Contains(textUpper)) ||\n                hg.Players.Any(pl => pl.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal));\n        }\n\n        /// <summary>\n        /// Checks if a game's broadcast options match the current filter criteria.\n        /// </summary>\n        /// <param name=\"game\">The hosted game to check.</param>\n        /// <returns>True if the game matches the filter criteria, false otherwise.</returns>\n        private bool GameOptionsMatch(HostedCnCNetGame game)\n        {\n            if (game.BroadcastedGameOptionValues == null)\n                return true;\n\n            var broadcastableSettings = gameLobby.GetBroadcastableSettings();\n\n            for (int i = 0; i < broadcastableSettings.Count; i++)\n            {\n                if (i >= game.BroadcastedGameOptionValues.Length)\n                    break;\n\n                int? filterValue = UserINISettings.Instance.GetGameOptionFilterValue(broadcastableSettings[i].Name);\n\n                if (filterValue == null)\n                    continue;\n\n                if (game.BroadcastedGameOptionValues[i] != filterValue.Value)\n                    return false;\n            }\n\n            return true;\n        }\n\n        private void OnCnCNetGameCountUpdated(object sender, PlayerCountEventArgs e) => UpdateOnlineCount(e.PlayerCount);\n\n        private void UpdateOnlineCount(int playerCount) => lblOnlineCount.Text = playerCount.ToString();\n\n        private void InitializeGameList()\n        {\n            int i = 0;\n\n            foreach (var game in gameCollection.GameList)\n            {\n                if (!game.Supported || string.IsNullOrEmpty(game.ChatChannel))\n                {\n                    continue;\n                }\n\n                var item = new XNADropDownItem();\n                item.Text = game.UIName;\n                item.Texture = game.Texture;\n\n                ddCurrentChannel.AddItem(item);\n\n                var chatChannel = connectionManager.FindChannel(game.ChatChannel);\n\n                if (chatChannel == null)\n                {\n                    chatChannel = connectionManager.CreateChannel(game.UIName, game.ChatChannel,\n                        true, true, \"ra1-derp\");\n                    connectionManager.AddChannel(chatChannel);\n                }\n\n                item.Tag = chatChannel;\n\n                if (!string.IsNullOrEmpty(game.GameBroadcastChannel))\n                {\n                    var gameBroadcastChannel = connectionManager.FindChannel(game.GameBroadcastChannel);\n\n                    if (gameBroadcastChannel == null)\n                    {\n                        gameBroadcastChannel = connectionManager.CreateChannel(\n                            string.Format(\"{0} Broadcast Channel\".L10N(\"Client:Main:BroadcastChannel\"), game.UIName),\n                            game.GameBroadcastChannel, true, false, null);\n                        connectionManager.AddChannel(gameBroadcastChannel);\n                    }\n\n                    gameBroadcastChannel.CTCPReceived += GameBroadcastChannel_CTCPReceived;\n                    gameBroadcastChannel.UserLeft += GameBroadcastChannel_UserLeftOrQuit;\n                    gameBroadcastChannel.UserQuitIRC += GameBroadcastChannel_UserLeftOrQuit;\n                    gameBroadcastChannel.UserKicked += GameBroadcastChannel_UserLeftOrQuit;\n                }\n\n                if (game.InternalName.ToUpper() == localGameID.ToUpper())\n                {\n                    ddCurrentChannel.SelectedIndex = i;\n                }\n\n                i++;\n            }\n\n            if (connectionManager.MainChannel == null)\n            {\n                // Set CnCNet channel as main channel if no channel found\n                ddCurrentChannel.SelectedIndex = ddCurrentChannel.Items.Count - 1;\n            }\n        }\n\n        private void PostUIInit()\n        {\n            sndGameCreated = new EnhancedSoundEffect(\"gamecreated.wav\");\n            sndGameInviteReceived = new EnhancedSoundEffect(\"pm.wav\");\n\n            cAdminNameColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AdminNameColor);\n\n            var assembly = Assembly.GetAssembly(typeof(GameCollection));\n            using Stream unknownIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.unknownicon.png\");\n            using Stream cncnetIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.cncneticon.png\");\n\n            unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream));\n            adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream));\n\n            connectionManager.WelcomeMessageReceived += ConnectionManager_WelcomeMessageReceived;\n            connectionManager.Disconnected += ConnectionManager_Disconnected;\n            connectionManager.PrivateCTCPReceived += ConnectionManager_PrivateCTCPReceived;\n\n            cncnetUserData.UserFriendToggled += RefreshPlayerList;\n            cncnetUserData.UserIgnoreToggled += RefreshPlayerList;\n\n            gameCreationPanel = new DarkeningPanel(WindowManager);\n            AddChild(gameCreationPanel);\n\n            GameCreationWindow gcw = new GameCreationWindow(WindowManager, tunnelHandler);\n            gameCreationPanel.AddChild(gcw);\n            gameCreationPanel.Tag = gcw;\n            gcw.Cancelled += Gcw_Cancelled;\n            gcw.GameCreated += Gcw_GameCreated;\n            gcw.LoadedGameCreated += Gcw_LoadedGameCreated;\n\n            gameCreationPanel.Hide();\n\n            string clientVersion = GitVersionInformation.AssemblySemVer;\n#if DEVELOPMENT_BUILD\n            clientVersion = $\"{GitVersionInformation.CommitDate} {GitVersionInformation.BranchName}@{GitVersionInformation.ShortSha}\";\n#endif\n\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, Renderer.GetSafeString(\n                    string.Format(\"*** CnCNet Client version {0} ***\".L10N(\"Client:Main:CnCNetClientVersionMessageV2\"), clientVersion),\n                    lbChatMessages.FontIndex)));\n\n            {\n                string developBuildWarningMessage = \"This is a development build of the client. Stability and reliability may not be fully guaranteed.\".L10N(\"Client:Main:DevelopmentBuildWarning\");\n\n#if DEVELOPMENT_BUILD\n                if (ClientConfiguration.Instance.ShowDevelopmentBuildWarnings)\n                {\n                    connectionManager.MainChannel.AddMessage(new ChatMessage(Color.Red, Renderer.GetSafeString(\n                            developBuildWarningMessage, lbChatMessages.FontIndex)));\n                }\n#endif\n            }\n\n            connectionManager.BannedFromChannel += ConnectionManager_BannedFromChannel;\n\n            loginWindow = new CnCNetLoginWindow(WindowManager);\n            loginWindow.Connect += LoginWindow_Connect;\n            loginWindow.Cancelled += LoginWindow_Cancelled;\n\n            var loginWindowPanel = new DarkeningPanel(WindowManager);\n            loginWindowPanel.Alpha = 0.0f;\n\n            AddChild(loginWindowPanel);\n            loginWindowPanel.AddChild(loginWindow);\n            loginWindow.Disable();\n\n            passwordRequestWindow = new PasswordRequestWindow(WindowManager, pmWindow);\n            passwordRequestWindow.PasswordEntered += PasswordRequestWindow_PasswordEntered;\n\n            var passwordRequestWindowPanel = new DarkeningPanel(WindowManager);\n            passwordRequestWindowPanel.Alpha = 0.0f;\n            AddChild(passwordRequestWindowPanel);\n            passwordRequestWindowPanel.AddChild(passwordRequestWindow);\n            passwordRequestWindow.Disable();\n\n            gameLobby.GameLeft += GameLobby_GameLeft;\n            gameLoadingLobby.GameLeft += GameLoadingLobby_GameLeft;\n\n            UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved;\n\n            GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted;\n            GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited;\n        }\n\n        /// <summary>\n        /// Displays a message when the IRC server has informed that the local user\n        /// has been banned from a channel that they're attempting to join.\n        /// </summary>\n        private void ConnectionManager_BannedFromChannel(object sender, ChannelEventArgs e)\n        {\n            var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName);\n\n            if (game == null)\n            {\n                var chatChannel = connectionManager.FindChannel(e.ChannelName);\n                chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format(\n                    \"Cannot join chat channel {0}, you're banned!\".L10N(\"Client:Main:PlayerBannedByChannel\"), chatChannel.UIName)));\n                return;\n            }\n\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format(\n                \"Cannot join game {0}, you've been banned by the game host!\".L10N(\"Client:Main:PlayerBannedByHost\"), game.RoomName)));\n\n            isJoiningGame = false;\n            if (gameOfLastJoinAttempt != null)\n            {\n                if (gameOfLastJoinAttempt.IsLoadedGame)\n                    gameLoadingLobby.Clear();\n                else\n                    gameLobby.Clear();\n            }\n        }\n\n        private void SharedUILogic_GameProcessStarted()\n        {\n            connectionManager.SendCustomMessage(new QueuedMessage(\"AWAY \" + (char)58 + \"In-game\",\n                QueuedMessageType.SYSTEM_MESSAGE, 0));\n        }\n\n        private void SharedUILogic_GameProcessExited()\n        {\n            connectionManager.SendCustomMessage(new QueuedMessage(\"AWAY\",\n                QueuedMessageType.SYSTEM_MESSAGE, 0));\n        }\n\n        private void Instance_SettingsSaved(object sender, EventArgs e)\n        {\n            if (!connectionManager.IsConnected)\n                return;\n\n            foreach (CnCNetGame game in gameCollection.GameList)\n            {\n                if (!game.Supported)\n                    continue;\n\n                if (game.InternalName.ToUpper() == localGameID)\n                    continue;\n\n                if (followedGames.Contains(game.InternalName) &&\n                    !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper()))\n                {\n                    connectionManager.FindChannel(game.GameBroadcastChannel).Leave();\n                    followedGames.Remove(game.InternalName);\n                }\n                else if (!followedGames.Contains(game.InternalName) &&\n                    UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper()))\n                {\n                    connectionManager.FindChannel(game.GameBroadcastChannel).Join();\n                    followedGames.Add(game.InternalName);\n                }\n            }\n        }\n\n        private void LbPlayerList_RightClick(object sender, EventArgs e)\n        {\n            lbPlayerList.SelectedIndex = lbPlayerList.HoveredIndex;\n\n            if (lbPlayerList.SelectedIndex < 0 ||\n                lbPlayerList.SelectedIndex >= lbPlayerList.Items.Count)\n            {\n                return;\n            }\n\n            var user = (ChannelUser)lbPlayerList.SelectedItem.Tag;\n\n            globalContextMenu.Show(user, GetCursorPoint());\n        }\n\n        private void LbChatMessages_RightClick(object sender, EventArgs e)\n        {\n            var item = lbChatMessages.HoveredItem;\n            var chatMessage = item?.Tag as ChatMessage;\n\n            ShowPlayerMessageContextMenu(chatMessage);\n        }\n\n        private void ShowPlayerMessageContextMenu(ChatMessage chatMessage)\n        {\n            lbChatMessages.SelectedIndex = lbChatMessages.HoveredIndex;\n\n            globalContextMenu.Show(chatMessage, GetCursorPoint());\n        }\n\n        /// <summary>\n        /// Enables private messaging by PM'ing a user in the player list.\n        /// </summary>\n        private void LbPlayerList_DoubleLeftClick(object sender, EventArgs e)\n        {\n            if (lbPlayerList.SelectedItem == null)\n                return;\n\n            var channelUser = (ChannelUser)lbPlayerList.SelectedItem.Tag;\n\n            pmWindow.InitPM(channelUser.IRCUser.Name);\n        }\n\n        /// <summary>\n        /// Hides the login dialog once the user has hit Connect on that dialog.\n        /// </summary>\n        private void LoginWindow_Connect(object sender, EventArgs e)\n        {\n            connectionManager.Connect();\n            loginWindow.Disable();\n\n            SetLogOutButtonText();\n        }\n\n        /// <summary>\n        /// Hides the login window and the CnCNet lobby if the user\n        /// cancels connecting to CnCNet in the login dialog.\n        /// </summary>\n        private void LoginWindow_Cancelled(object sender, EventArgs e)\n        {\n            topBar.SwitchToPrimary();\n            loginWindow.Disable();\n        }\n\n        private void GameLoadingLobby_GameLeft(object sender, EventArgs e)\n        {\n            topBar.SwitchToSecondary();\n            isInGameRoom = false;\n            SetLogOutButtonText();\n\n            // keep the friends window up to date so it can disable the Invite option\n            pmWindow.ClearInviteChannelInfo();\n        }\n\n        private void GameLobby_GameLeft(object sender, EventArgs e)\n        {\n            topBar.SwitchToSecondary();\n            isInGameRoom = false;\n            SetLogOutButtonText();\n\n            // keep the friends window up to date so it can disable the Invite option\n            pmWindow.ClearInviteChannelInfo();\n        }\n\n        private void SetLogOutButtonText()\n        {\n            if (isInGameRoom)\n            {\n                btnLogout.Text = \"Game Lobby\".L10N(\"Client:Main:GameLobby\");\n                return;\n            }\n\n            if (UserINISettings.Instance.PersistentMode)\n            {\n                btnLogout.Text = \"Main Menu\".L10N(\"Client:Main:MainMenu\");\n                return;\n            }\n\n            btnLogout.Text = \"Log Out\".L10N(\"Client:Main:LogOut\");\n        }\n\n        private void BtnJoinGame_LeftClick(object sender, EventArgs e) => JoinSelectedGame();\n\n        private void LbGameList_DoubleLeftClick(object sender, EventArgs e) => JoinSelectedGame();\n\n        private void LbGameList_RightClick(object sender, EventArgs e)\n        {\n            lbGameList.SelectedIndex = lbGameList.HoveredIndex;\n\n            var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag;\n            if (listedGame == null)\n                return;\n\n            globalContextMenu.Show(listedGame.HostName, GetCursorPoint());\n        }\n\n        private void PasswordRequestWindow_PasswordEntered(object sender, PasswordEventArgs e) => _JoinGame(e.HostedGame, e.Password);\n\n        private string GetJoinGameErrorBase()\n        {\n            if (isJoiningGame)\n                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\");\n\n            if (ProgramConstants.IsInGame)\n                return \"Cannot join game while the main game executable is running.\".L10N(\"Client:Main:JoinGameErrorGameRunning\");\n\n            return null;\n        }\n        /// <summary>\n        /// Checks if the user can join a game.\n        /// Returns null if the user can, otherwise returns an error message\n        /// that tells the reason why the user cannot join the game.\n        /// </summary>\n        /// <param name=\"gameIndex\">The index of the game in the game list box.</param>\n        private string GetJoinGameErrorByIndex(int gameIndex)\n        {\n            if (gameIndex < 0 || gameIndex >= lbGameList.HostedGames.Count)\n                return \"Invalid game index\".L10N(\"Client:Main:InvalidGameIndex\");\n\n            return GetJoinGameErrorBase();\n        }\n\n        /// <summary>\n        /// Returns an error message if game is not join-able, otherwise null.\n        /// </summary>\n        /// <param name=\"hg\"></param>\n        /// <returns></returns>\n        private string GetJoinGameError(HostedCnCNetGame hg)\n        {\n            if (hg.Game.InternalName.ToUpper() != localGameID.ToUpper())\n                return string.Format(\"The selected game is for {0}!\".L10N(\"Client:Main:GameIsOfPurpose\"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName));\n\n            if (hg.Incompatible && ClientConfiguration.Instance.DisallowJoiningIncompatibleGames)\n                return \"Cannot join game. The host is on a different game version than you.\".L10N(\"Client:Main:DisallowJoiningIncompatibleGames\");\n\n            if (hg.Locked)\n                return string.Format(\"The game {0} is locked!\".L10N(\"Client:Main:GameLockedWithName\"), hg.RoomName);\n\n            if (hg.IsLoadedGame && !hg.Players.Contains(ProgramConstants.PLAYERNAME))\n                return \"You do not exist in the saved game!\".L10N(\"Client:Main:NotInSavedGame\");\n\n            return GetJoinGameErrorBase();\n        }\n\n        private void JoinSelectedGame()\n        {\n            var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag;\n            if (listedGame == null)\n                return;\n            var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame);\n            JoinGameByIndex(hostedGameIndex, string.Empty);\n        }\n\n        private bool JoinGameByIndex(int gameIndex, string password)\n        {\n            string error = GetJoinGameErrorByIndex(gameIndex);\n            if (!string.IsNullOrEmpty(error))\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, error));\n                return false;\n            }\n\n            return JoinGame((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel);\n        }\n\n        /// <summary>\n        /// Attempt to join a game.\n        /// </summary>\n        /// <param name=\"hg\">The game to join.</param>\n        /// <param name=\"password\">The password to join with.</param>\n        /// <param name=\"messageView\">The message view/list to write error messages to.</param>\n        /// <returns></returns>\n        private bool JoinGame(HostedCnCNetGame hg, string password, IMessageView messageView)\n        {\n            string error = GetJoinGameError(hg);\n            if (!string.IsNullOrEmpty(error))\n            {\n                messageView.AddMessage(new ChatMessage(Color.White, error));\n                return false;\n            }\n\n            if (isInGameRoom)\n            {\n                topBar.SwitchToPrimary();\n                return false;\n            }\n\n            if (hg.GameVersion != ProgramConstants.GAME_VERSION)\n                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\")));\n\n            if (hg.Passworded)\n            {\n                // only display password dialog if we've not been supplied with a password (invite)\n                if (string.IsNullOrEmpty(password))\n                {\n                    passwordRequestWindow.SetHostedGame(hg);\n                    passwordRequestWindow.Enable();\n                    return true;\n                }\n            }\n            else\n            {\n                if (!hg.IsLoadedGame)\n                {\n                    password = Utilities.CalculateSHA1ForString\n                        (hg.ChannelName).Substring(0, 10);\n                }\n                else\n                {\n                    IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Saved Games\", \"spawnSG.ini\"));\n                    password = Utilities.CalculateSHA1ForString(\n                        spawnSGIni.GetStringValue(\"Settings\", \"GameID\", string.Empty)).Substring(0, 10);\n                }\n            }\n\n            _JoinGame(hg, password);\n\n            return true;\n        }\n\n        private void _JoinGame(HostedCnCNetGame hg, string password)\n        {\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White,\n                string.Format(\"Attempting to join game {0} ...\".L10N(\"Client:Main:AttemptJoin\"), hg.RoomName)));\n            isJoiningGame = true;\n            gameOfLastJoinAttempt = hg;\n\n            Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password);\n            connectionManager.AddChannel(gameChannel);\n\n            if (hg.IsLoadedGame)\n            {\n                gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName);\n                gameChannel.UserAdded += GameLoadingChannel_UserAdded;\n                //gameChannel.MessageAdded += GameLoadingChannel_MessageAdded;\n                gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_LoadedGame;\n                isJoiningGame = false;\n            }\n            else\n            {\n                gameLobby.SetUp(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, hg.SkillLevel);\n                gameChannel.UserAdded += GameChannel_UserAdded;\n                gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_NewGame;\n                gameChannel.InviteOnlyErrorOnJoin += GameChannel_InviteOnlyErrorOnJoin;\n                gameChannel.ChannelFull += GameChannel_ChannelFull;\n                gameChannel.TargetChangeTooFast += GameChannel_TargetChangeTooFast;\n            }\n\n            connectionManager.SendCustomMessage(new QueuedMessage(\"JOIN \" + hg.ChannelName + \" \" + password,\n                QueuedMessageType.INSTANT_MESSAGE, 0));\n        }\n\n        private void GameChannel_TargetChangeTooFast(object sender, MessageEventArgs e)\n        {\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message));\n            ClearGameJoinAttempt((Channel)sender);\n        }\n\n        private void GameChannel_ChannelFull(object sender, EventArgs e) =>\n            // We'd do the exact same things here, so we can just call the method below\n            GameChannel_InviteOnlyErrorOnJoin(sender, e);\n\n        private void GameChannel_InviteOnlyErrorOnJoin(object sender, EventArgs e)\n        {\n            var channel = (Channel)sender;\n\n            var game = FindGameByChannelName(channel.ChannelName);\n            if (game != null)\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format(\"The game {0} is locked!\".L10N(\"Client:Main:GameLockedWithName\"), game.RoomName)));\n                game.Locked = true;\n                SortAndRefreshHostedGames();\n            }\n            else\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, \"The selected game is locked!\".L10N(\"Client:Main:GameLocked\")));\n            }\n\n            ClearGameJoinAttempt((Channel)sender);\n        }\n\n        private HostedCnCNetGame FindGameByChannelName(string channelName)\n        {\n            var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == channelName);\n            if (game == null)\n                return null;\n\n            return (HostedCnCNetGame)game;\n        }\n\n        private void GameChannel_InvalidPasswordEntered_NewGame(object sender, EventArgs e)\n        {\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, \"Incorrect password!\".L10N(\"Client:Main:PasswordWrong\")));\n            ClearGameJoinAttempt((Channel)sender);\n        }\n\n        private void GameChannel_UserAdded(object sender, Online.ChannelUserEventArgs e)\n        {\n            Channel gameChannel = (Channel)sender;\n\n            if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME)\n            {\n                ClearGameChannelEvents(gameChannel);\n                gameLobby.OnJoined();\n                isInGameRoom = true;\n                SetLogOutButtonText();\n            }\n        }\n\n        private void ClearGameJoinAttempt(Channel channel)\n        {\n            ClearGameChannelEvents(channel);\n            gameLobby.Clear();\n        }\n\n        private void ClearGameChannelEvents(Channel channel)\n        {\n            channel.UserAdded -= GameChannel_UserAdded;\n            channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_NewGame;\n            channel.InviteOnlyErrorOnJoin -= GameChannel_InviteOnlyErrorOnJoin;\n            channel.ChannelFull -= GameChannel_ChannelFull;\n            channel.TargetChangeTooFast -= GameChannel_TargetChangeTooFast;\n            isJoiningGame = false;\n        }\n\n        private void BtnNewGame_LeftClick(object sender, EventArgs e)\n        {\n            if (isInGameRoom)\n            {\n                topBar.SwitchToPrimary();\n                return;\n            }\n\n            gameCreationPanel.Show();\n            var gcw = (GameCreationWindow)gameCreationPanel.Tag;\n\n            gcw.Refresh();\n        }\n\n        private void Gcw_GameCreated(object sender, GameCreationEventArgs e)\n        {\n            if (gameLobby.Enabled || gameLoadingLobby.Enabled)\n                return;\n\n            string channelName = RandomizeChannelName();\n            string password = e.Password;\n            bool isCustomPassword = true;\n            if (string.IsNullOrEmpty(password))\n            {\n                password = Utilities.CalculateSHA1ForString(\n                    channelName).Substring(0, 10);\n                isCustomPassword = false;\n            }\n\n            Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password);\n            connectionManager.AddChannel(gameChannel);\n            gameLobby.SetUp(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, e.SkillLevel);\n            gameChannel.UserAdded += GameChannel_UserAdded;\n            //gameChannel.MessageAdded += GameChannel_MessageAdded;\n            connectionManager.SendCustomMessage(new QueuedMessage(\"JOIN \" + channelName + \" \" + password,\n                QueuedMessageType.INSTANT_MESSAGE, 0));\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White,\n               string.Format(\"Creating a game named {0} ...\".L10N(\"Client:Main:CreateGameNamed\"), e.GameRoomName)));\n\n            gameCreationPanel.Hide();\n\n            // update the friends window so it can enable the Invite option\n            pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password);\n        }\n\n        private void Gcw_LoadedGameCreated(object sender, GameCreationEventArgs e)\n        {\n            if (gameLobby.Enabled || gameLoadingLobby.Enabled)\n                return;\n\n            string channelName = RandomizeChannelName();\n\n            Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password);\n            connectionManager.AddChannel(gameLoadingChannel);\n            gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME);\n            gameLoadingChannel.UserAdded += GameLoadingChannel_UserAdded;\n            connectionManager.SendCustomMessage(new QueuedMessage(\"JOIN \" + channelName + \" \" + e.Password,\n                QueuedMessageType.INSTANT_MESSAGE, 0));\n            connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White,\n               string.Format(\"Creating a game named {0} ...\".L10N(\"Client:Main:CreateGameNamed\"), e.GameRoomName)));\n\n            gameCreationPanel.Hide();\n\n            // update the friends window so it can enable the Invite option\n            pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password);\n        }\n\n        private void GameChannel_InvalidPasswordEntered_LoadedGame(object sender, EventArgs e)\n        {\n            var channel = (Channel)sender;\n            channel.UserAdded -= GameLoadingChannel_UserAdded;\n            channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame;\n            gameLoadingLobby.Clear();\n            isJoiningGame = false;\n        }\n\n        private void GameLoadingChannel_UserAdded(object sender, ChannelUserEventArgs e)\n        {\n            Channel gameLoadingChannel = (Channel)sender;\n\n            if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME)\n            {\n                gameLoadingChannel.UserAdded -= GameLoadingChannel_UserAdded;\n                gameLoadingChannel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame;\n\n                gameLoadingLobby.OnJoined();\n                isInGameRoom = true;\n                isJoiningGame = false;\n            }\n        }\n\n        /// <summary>\n        /// Generates and returns a random, unused cannel name.\n        /// </summary>\n        /// <returns>A random channel name based on the currently played game.</returns>\n        private string RandomizeChannelName()\n        {\n            int maxTries = 10000;\n            for (int i = 0; i < maxTries; i++)\n            {\n                string channelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID) + \"-game\" + random.Next(1000000, 9999999);\n                int index = lbGameList.HostedGames.FindIndex(c => ((HostedCnCNetGame)c).ChannelName == channelName);\n                if (index == -1)\n                    return channelName;\n            }\n\n            throw new Exception(string.Format(\"Could not find a random channel name after {0} retries\", maxTries));\n        }\n\n        private void Gcw_Cancelled(object sender, EventArgs e) => gameCreationPanel.Hide();\n\n        private void TbChatInput_EnterPressed(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbChatInput.Text))\n                return;\n\n            IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag;\n\n            currentChatChannel.SendChatMessage(tbChatInput.Text, selectedColor);\n\n            tbChatInput.Text = string.Empty;\n        }\n\n        private void SetChatColor()\n        {\n            IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag;\n            tbChatInput.TextColor = selectedColor.XnaColor;\n            gameLobby.ChangeChatColor(selectedColor);\n            gameLoadingLobby.ChangeChatColor(selectedColor);\n            UserINISettings.Instance.ChatColor.Value = ddColor.SelectedIndex;\n        }\n\n        private void DdColor_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            SetChatColor();\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        private void ConnectionManager_Disconnected(object sender, EventArgs e)\n        {\n            btnNewGame.AllowClick = false;\n            btnJoinGame.AllowClick = false;\n            ddCurrentChannel.AllowDropDown = false;\n            tbChatInput.Enabled = false;\n            lbPlayerList.Clear();\n\n            lbGameList.ClearGames();\n            followedGames.Clear();\n\n            gameCreationPanel.Hide();\n\n            // Switch channel to default\n            if (localGame != null)\n            {\n                int gameIndex = ddCurrentChannel.Items.FindIndex(i => i.Text == localGame.UIName);\n                if (gameIndex > -1)\n                    ddCurrentChannel.SelectedIndex = gameIndex;\n            }\n\n            if (gameCheckCancellation != null)\n                gameCheckCancellation.Cancel();\n        }\n\n        private void ConnectionManager_WelcomeMessageReceived(object sender, EventArgs e)\n        {\n            btnNewGame.AllowClick = true;\n            btnJoinGame.AllowClick = true;\n            ddCurrentChannel.AllowDropDown = true;\n            tbChatInput.Enabled = true;\n\n            Channel cncnetChannel = connectionManager.FindChannel(\"#cncnet\");\n            cncnetChannel?.Join();\n\n            string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID);\n            connectionManager.FindChannel(localGameChatChannelName).Join();\n\n            string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID);\n            connectionManager.FindChannel(localGameBroadcastChannel).Join();\n\n            foreach (CnCNetGame game in gameCollection.GameList)\n            {\n                if (!game.Supported)\n                    continue;\n\n                if (game.InternalName.ToUpper() != localGameID)\n                {\n                    if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper()))\n                    {\n                        connectionManager.FindChannel(game.GameBroadcastChannel).Join();\n                        followedGames.Add(game.InternalName);\n                    }\n                }\n            }\n\n            gameCheckCancellation = new CancellationTokenSource();\n            CnCNetGameCheck.Instance.InitializeService(gameCheckCancellation);\n        }\n\n        private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEventArgs e)\n        {\n            foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers)\n            {\n                if (cmdHandler.Handle(e.Sender, e.Message))\n                    return;\n            }\n\n            Logger.Log(\"Unhandled private CTCP command: \" + e.Message + \" from \" + e.Sender);\n        }\n\n        private void HandleGameInviteCommand(string sender, string argumentsString)\n        {\n            // arguments are semicolon-delimited\n            var arguments = argumentsString.Split(';');\n\n            // we expect to be given a channel name, a (human-friendly) game name and optionally a password\n            if (arguments.Length < 2 || arguments.Length > 3)\n                return;\n\n            string channelName = arguments[0];\n            string gameName = arguments[1];\n            string password = (arguments.Length == 3) ? arguments[2] : string.Empty;\n\n            if (!CanReceiveInvitationMessagesFrom(sender))\n                return;\n\n            var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName);\n\n            // also enforce user preference on whether to accept invitations from non-friends\n            // this is kept separate from CanReceiveInvitationMessagesFrom() as we still\n            // want to let the host know that we couldn't receive the invitation\n            if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) ||\n                (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly &&\n                !cncnetUserData.IsFriend(sender)))\n            {\n                // let the host know that we can't accept\n                // note this is not reached for the rejection case\n                connectionManager.SendCustomMessage(new QueuedMessage(\"PRIVMSG \" + sender + \" :\\u0001\" +\n                    ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + \"\\u0001\",\n                    QueuedMessageType.CHAT_MESSAGE, 0));\n\n                return;\n            }\n\n            // if there's already an outstanding invitation from this user/channel combination,\n            // we don't want to display another\n            // we won't bother telling the host though, since their old invitation is still\n            // available to us\n            var invitationIdentity = new UserChannelPair(sender, channelName);\n\n            if (invitationIndex.ContainsKey(invitationIdentity))\n            {\n                return;\n            }\n\n            var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager);\n\n            WindowManager.AddAndInitializeControl(gameInviteChoiceBox);\n\n            // show the invitation at top left; it will remain until it is acted upon or the target game is closed\n            gameInviteChoiceBox.Show(\n                \"GAME INVITATION\".L10N(\"Client:Main:GameInviteTitle\"),\n                GetUserTexture(sender),\n                sender,\n                string.Format(\"Join {0}?\".L10N(\"Client:Main:GameInviteText\"), gameName),\n                \"Yes\".L10N(\"Client:Main:ButtonYes\"), \"No\".L10N(\"Client:Main:ButtonNo\"), 0);\n\n            // add the invitation to the index so we can remove it if the target game is closed\n            // also lets us silently ignore new invitations from the same person while this one is still outstanding\n            invitationIndex[invitationIdentity] =\n                new WeakReference(gameInviteChoiceBox);\n\n            gameInviteChoiceBox.AffirmativeClickedAction = delegate (ChoiceNotificationBox choiceBox)\n            {\n                // if we're currently in a game lobby, first leave that channel\n                if (isInGameRoom)\n                {\n                    gameLobby.LeaveGameLobby();\n                    gameLoadingLobby.Clear();\n                }\n\n                // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist\n                if (!JoinGameByIndex(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password))\n                {\n                    XNAMessageBox.Show(WindowManager,\n                        \"Failed to join\".L10N(\"Client:Main:JoinFailedTitle\"),\n                        string.Format(\"Unable to join {0}'s game. The game may be locked or closed.\".L10N(\"Client:Main:JoinFailedText\"), sender));\n                }\n\n                // clean up the index as this invitation no longer exists\n                invitationIndex.Remove(invitationIdentity);\n            };\n\n            gameInviteChoiceBox.NegativeClickedAction = delegate (ChoiceNotificationBox choiceBox)\n            {\n                // clean up the index as this invitation no longer exists\n                invitationIndex.Remove(invitationIdentity);\n            };\n\n            sndGameInviteReceived.Play();\n        }\n\n        private void HandleGameInvitationFailedNotification(string sender)\n        {\n            if (!CanReceiveInvitationMessagesFrom(sender))\n                return;\n\n            if (isInGameRoom && !ProgramConstants.IsInGame)\n            {\n                gameLobby.AddWarning(\n                    string.Format((\"{0} could not receive your invitation. They might be in game \" +\n                    \"or only accepting invitations from friends. Ensure your game is \" +\n                    \"unlocked and visible in the lobby before trying again.\").L10N(\"Client:Main:InviteNotDelivered\"), sender));\n            }\n        }\n\n        private void DdCurrentChannel_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (currentChatChannel != null)\n            {\n                currentChatChannel.UserAdded -= RefreshPlayerList;\n                currentChatChannel.UserLeft -= RefreshPlayerList;\n                currentChatChannel.UserQuitIRC -= RefreshPlayerList;\n                currentChatChannel.UserKicked -= RefreshPlayerList;\n                currentChatChannel.UserListReceived -= RefreshPlayerList;\n                currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded;\n                currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated;\n\n                if (currentChatChannel.ChannelName != \"#cncnet\" &&\n                    currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID))\n                {\n                    // Remove the assigned channels from the users so we don't have ghost users on the PM user list\n                    currentChatChannel.Users.DoForAllUsers(user =>\n                    {\n                        connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName);\n                    });\n\n                    currentChatChannel.Leave();\n                }\n            }\n\n            currentChatChannel = (Channel)ddCurrentChannel.SelectedItem?.Tag;\n            if (currentChatChannel == null)\n                throw new Exception(\"Current selected chat channel is null. This should not happen.\");\n\n            currentChatChannel.UserAdded += RefreshPlayerList;\n            currentChatChannel.UserLeft += RefreshPlayerList;\n            currentChatChannel.UserQuitIRC += RefreshPlayerList;\n            currentChatChannel.UserKicked += RefreshPlayerList;\n            currentChatChannel.UserListReceived += RefreshPlayerList;\n            currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded;\n            currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated;\n            connectionManager.SetMainChannel(currentChatChannel);\n\n            lbPlayerList.TopIndex = 0;\n\n            lbChatMessages.TopIndex = 0;\n            lbChatMessages.Clear();\n            OnChatMessagesCleared();\n            currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg));\n\n            RefreshPlayerList(this, EventArgs.Empty);\n\n            if (currentChatChannel.ChannelName != \"#cncnet\" &&\n                currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID))\n            {\n                currentChatChannel.Join();\n            }\n        }\n\n        private void RefreshPlayerList(object sender, EventArgs e)\n        {\n            string selectedUserName = lbPlayerList.SelectedItem == null ?\n                string.Empty : lbPlayerList.SelectedItem.Text;\n\n            lbPlayerList.Clear();\n\n            // Note: IUserCollection.GetFirst() is not guaranteed to be implemented, unless it is a SortedUserCollection\n            Debug.Assert(currentChatChannel.Users is SortedUserCollection<ChannelUser>, \"Channel 'users' is supposed to be a SortedUserCollection\");\n            var current = currentChatChannel.Users.GetFirst();\n            while (current != null)\n            {\n                var user = current.Value;\n                user.IRCUser.IsFriend = cncnetUserData.IsFriend(user.IRCUser.Name);\n                user.IRCUser.IsIgnored = cncnetUserData.IsIgnored(user.IRCUser.Ident);\n                lbPlayerList.AddUser(user);\n                current = current.Next;\n            }\n\n            if (selectedUserName != string.Empty)\n            {\n                lbPlayerList.SelectedIndex = lbPlayerList.Items.FindIndex(\n                    i => i.Text == selectedUserName);\n            }\n        }\n\n        /// <summary>\n        /// Refreshes a single user's info on the player list.\n        /// </summary>\n        /// <param name=\"user\">User on the current chat channel.</param>\n        private void RefreshPlayerListUser(ChannelUser user)\n        {\n            user.IRCUser.IsFriend = cncnetUserData.IsFriend(user.IRCUser.Name);\n            user.IRCUser.IsIgnored = cncnetUserData.IsIgnored(user.IRCUser.Ident);\n            lbPlayerList.UpdateUserInfo(user);\n        }\n\n        private void CurrentChatChannel_UserGameIndexUpdated(object sender, ChannelUserEventArgs e)\n        {\n            var ircUser = e.User.IRCUser;\n            var item = lbPlayerList.Items.Find(i => i.Text.StartsWith(ircUser.Name));\n\n            if (ircUser.GameID < 0 || ircUser.GameID >= gameCollection.GameList.Count)\n                item.Texture = unknownGameIcon;\n            else\n                item.Texture = gameCollection.GameList[ircUser.GameID].Texture;\n        }\n\n        private void OnChatMessagesCleared()\n        {\n            ctcpInvalidGameMessageShown = false;\n            ctcpNoTunnelMessageShown = false;\n            ctcpNoTunnelForGamesMessageShown = false;\n        }\n\n        private void AddMessageToChat(ChatMessage message)\n        {\n            if (!string.IsNullOrEmpty(message.SenderIdent) &&\n                cncnetUserData.IsIgnored(message.SenderIdent) &&\n                !message.SenderIsAdmin)\n            {\n                lbChatMessages.AddMessage(new ChatMessage(Color.Silver, string.Format(\"Message blocked from - {0}\".L10N(\"Client:Main:PMBlockedFrom\"), message.SenderName)));\n            }\n            else\n            {\n                lbChatMessages.AddMessage(message);\n            }\n        }\n\n        private void CurrentChatChannel_MessageAdded(object sender, IRCMessageEventArgs e) =>\n            AddMessageToChat(e.Message);\n\n        /// <summary>\n        /// Removes a game from the list when the host quits CnCNet or\n        /// leaves the game broadcast channel.\n        /// </summary>\n        private void GameBroadcastChannel_UserLeftOrQuit(object sender, UserNameEventArgs e)\n        {\n            int gameIndex = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName);\n\n            if (gameIndex > -1)\n            {\n                lbGameList.RemoveGame(gameIndex);\n\n                // dismiss any outstanding invitations that are no longer valid\n                DismissInvalidInvitations();\n            }\n        }\n\n        private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventArgs e)\n        {\n            var channel = (Channel)sender;\n\n            var channelUser = channel.Users.Find(e.UserName);\n\n            if (channelUser == null)\n                return;\n\n            if (localGame != null &&\n                channel.ChannelName == localGame.GameBroadcastChannel &&\n                !updateDenied &&\n                channelUser.IsAdmin &&\n                !isInGameRoom &&\n                e.Message.StartsWith(\"UPDATE \") &&\n                e.Message.Length > 7)\n            {\n                string version = e.Message.Substring(7);\n                if (version != ProgramConstants.GAME_VERSION)\n                {\n                    var updateMessageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, \"Update available\".L10N(\"Client:Main:UpdateAvailableTitle\"),\n                        \"An update is available. Do you want to perform the update now?\".L10N(\"Client:Main:UpdateAvailableText\"));\n                    updateMessageBox.NoClickedAction = UpdateMessageBox_NoClicked;\n                    updateMessageBox.YesClickedAction = UpdateMessageBox_YesClicked;\n                }\n            }\n\n            if (!e.Message.StartsWith(\"GAME \"))\n                return;\n\n            string msg = e.Message.Substring(5); // Cut out GAME part\n            string[] splitMessage = msg.Split(new char[] { ';' });\n\n            if (splitMessage.Length != 14)\n            {\n                Logger.Log(\"Ignoring CTCP game message because of an invalid amount of parameters.\");\n\n                // Remind users that the network is good but the client is outdated or newer\n                if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpInvalidGameMessageShown)\n                {\n                    ctcpInvalidGameMessageShown = true;\n\n                    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. \" +\n                        \"You can ignore this prompt if there are games listed later. \" +\n                        \"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\");\n\n                    lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message));\n                }\n\n                return;\n            }\n\n            try\n            {\n                string revision = splitMessage[0];\n                if (revision != ProgramConstants.CNCNET_PROTOCOL_REVISION)\n                    return;\n                string gameVersion = splitMessage[1];\n                int maxPlayers = Conversions.IntFromString(splitMessage[2], 0);\n                string gameRoomChannelName = splitMessage[3];\n                string gameRoomDisplayName = splitMessage[4];\n                bool locked = Conversions.BooleanFromString(splitMessage[5].Substring(0, 1), true);\n                bool isCustomPassword = Conversions.BooleanFromString(splitMessage[5].Substring(1, 1), false);\n                bool isClosed = Conversions.BooleanFromString(splitMessage[5].Substring(2, 1), true);\n                bool isLoadedGame = Conversions.BooleanFromString(splitMessage[5].Substring(3, 1), false);\n                bool isLadder = Conversions.BooleanFromString(splitMessage[5].Substring(4, 1), false);\n                string[] players = splitMessage[6].Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);\n                List<string> playerNames = players.ToList();\n                string mapName = splitMessage[7];\n                string gameMode = splitMessage[8];\n\n                string[] tunnelAddressAndPort = splitMessage[9].Split(':');\n                string tunnelAddress = tunnelAddressAndPort[0];\n                int tunnelPort = int.Parse(tunnelAddressAndPort[1]);\n\n                string loadedGameId = splitMessage[10];\n                int skillLevel = int.Parse(splitMessage[11]);\n                string mapHash = splitMessage[12];\n\n                int[] gameOptionValues = null;\n\n                // Games with different versions may have different option counts, so ignore\n                if (gameVersion == ProgramConstants.GAME_VERSION && channel.ChannelName == localGame?.GameBroadcastChannel)\n                {\n                    var broadcastableSettings = gameLobby.GetBroadcastableSettings();\n                    if (broadcastableSettings.Count == 0)\n                    {\n                        gameOptionValues = null;\n                    }\n                    else if (!string.IsNullOrEmpty(splitMessage[13]))\n                    {\n                        gameOptionValues = new int[broadcastableSettings.Count];\n                        string[] allValueStrings = splitMessage[13].Split(',');\n\n                        int checkboxCount = gameLobby.CheckBoxes.Count(cb => cb.BroadcastToLobby);\n                        int packedCheckboxCount = (checkboxCount + 31) / 32;\n\n                        // packed checkbox values\n                        if (checkboxCount > 0 && allValueStrings.Length >= packedCheckboxCount)\n                        {\n                            int[] packedCheckboxes = new int[packedCheckboxCount];\n                            for (int i = 0; i < packedCheckboxCount; i++)\n                                packedCheckboxes[i] = int.Parse(allValueStrings[i]);\n\n                            for (int i = 0; i < checkboxCount; i++)\n                            {\n                                int packedIndex = i / 32;\n                                int bitIndex = i % 32;\n                                gameOptionValues[i] = (packedCheckboxes[packedIndex] & (1 << bitIndex)) != 0 ? 1 : 0;\n                            }\n                        }\n\n                        // dropdown indices\n                        int dropdownCount = gameLobby.DropDowns.Count(dd => dd.BroadcastToLobby);\n                        if (dropdownCount > 0)\n                        {\n                            int count = Math.Min(allValueStrings.Length - packedCheckboxCount, dropdownCount);\n                            for (int i = 0; i < count; i++)\n                                gameOptionValues[checkboxCount + i] = int.Parse(allValueStrings[packedCheckboxCount + i]);\n                        }\n                    }\n                }\n\n                CnCNetGame cncnetGame = gameCollection.GameList.Find(g => g.GameBroadcastChannel == channel.ChannelName);\n\n                if (cncnetGame == null)\n                    return;\n\n                // Find the tunnel server specified in the game message\n\n                if (tunnelHandler.Tunnels.Count == 0)\n                {\n                    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?\");\n\n                    // Remind users that the game is ignored because of no tunnel\n                    if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpNoTunnelMessageShown)\n                    {\n                        ctcpNoTunnelMessageShown = true;\n                        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. \" +\n                            \"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\");\n\n                        lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message));\n                    }\n\n                    return;\n                }\n\n                CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort);\n\n                if (tunnel == null)\n                {\n                    Logger.Log(string.Format(\"Ignoring CTCP game message because the specified tunnel {0}:{1} is not available. Available tunnel count: {2}\",\n                        tunnelAddress, tunnelPort, tunnelHandler.Tunnels.Count));\n\n                    // Remind users that the game is ignored because of no specified tunnel\n                    if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpNoTunnelForGamesMessageShown)\n                    {\n                        ctcpNoTunnelForGamesMessageShown = true;\n\n                        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. \" +\n                            \"You can ignore this prompt if there are games listed later. Otherwise, please contact support at {0}.\").L10N(\"Client:Main:NoTunnelForGames\"), ClientConfiguration.Instance.LongSupportURL);\n\n                        lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message));\n                    }\n\n                    return;\n                }\n\n                HostedCnCNetGame game = new HostedCnCNetGame(gameRoomChannelName, revision, gameVersion, maxPlayers,\n                    gameRoomDisplayName, isCustomPassword, true, players,\n                    e.UserName, mapName, gameMode, mapHash);\n                game.IsLoadedGame = isLoadedGame;\n                game.MatchID = loadedGameId;\n                game.LastRefreshTime = DateTime.Now;\n                game.IsLadder = isLadder;\n                game.Game = cncnetGame;\n                game.Locked = locked || (game.IsLoadedGame && !game.Players.Contains(ProgramConstants.PLAYERNAME));\n                game.Incompatible = cncnetGame == localGame && game.GameVersion != ProgramConstants.GAME_VERSION;\n                game.TunnelServer = tunnel;\n                game.SkillLevel = skillLevel;\n                game.BroadcastedGameOptionValues = gameOptionValues;\n\n                if (isClosed)\n                {\n                    int index = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName);\n\n                    if (index > -1)\n                    {\n                        lbGameList.RemoveGame(index);\n\n                        // dismiss any outstanding invitations that are no longer valid\n                        DismissInvalidInvitations();\n                    }\n\n                    return;\n                }\n\n                // Seek for the game in the internal game list based on the name of its host;\n                // if found, then refresh that game's information, otherwise add as new game\n                int gameIndex = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName);\n\n                if (gameIndex > -1)\n                {\n                    lbGameList.HostedGames[gameIndex] = game;\n                }\n                else\n                {\n                    if (UserINISettings.Instance.PlaySoundOnGameHosted &&\n                        cncnetGame.InternalName == localGameID.ToLower() &&\n                        !ProgramConstants.IsInGame && !game.Locked)\n                    {\n                        SoundPlayer.Play(sndGameCreated);\n                    }\n\n                    lbGameList.AddGame(game);\n                }\n                SortAndRefreshHostedGames();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Game parsing error: \" + ex.ToString());\n            }\n        }\n\n        private void UpdateMessageBox_YesClicked(XNAMessageBox messageBox) =>\n            UpdateCheck?.Invoke(this, EventArgs.Empty);\n\n        private void UpdateMessageBox_NoClicked(XNAMessageBox messageBox) => updateDenied = true;\n\n        private void BtnLogout_LeftClick(object sender, EventArgs e)\n        {\n            if (isInGameRoom)\n            {\n                topBar.SwitchToPrimary();\n                return;\n            }\n\n            if (connectionManager.IsConnected &&\n                !UserINISettings.Instance.PersistentMode)\n            {\n                connectionManager.Disconnect();\n            }\n\n            topBar.SwitchToPrimary();\n        }\n\n        public void SwitchOn()\n        {\n            Enable();\n\n            if (!connectionManager.IsConnected && !connectionManager.IsAttemptingConnection)\n            {\n                loginWindow.Enable();\n                loginWindow.LoadSettings();\n            }\n\n            SetLogOutButtonText();\n        }\n\n        public void SwitchOff() => Disable();\n\n        public string GetSwitchName() => \"CnCNet Lobby\".L10N(\"Client:Main:CnCNetLobby\");\n\n        private bool CanReceiveInvitationMessagesFrom(string username)\n        {\n            IRCUser iu = connectionManager.UserList.Find(u => u.Name == username);\n\n            // We don't accept invitation messages from people who we don't share any channels with\n            if (iu == null)\n            {\n                return false;\n            }\n\n            // Invitation messages from users we've blocked are not wanted\n            if (cncnetUserData.IsIgnored(iu.Ident))\n            {\n                return false;\n            }\n\n            return true;\n        }\n\n        private Texture2D GetUserTexture(string username)\n        {\n            Texture2D senderGameIcon = unknownGameIcon;\n\n            IRCUser iu = connectionManager.UserList.Find(u => u.Name == username);\n\n            if (iu != null && iu.GameID >= 0 && iu.GameID < gameCollection.GameList.Count)\n            {\n                senderGameIcon = gameCollection.GameList[iu.GameID].Texture;\n            }\n\n            return senderGameIcon;\n        }\n\n        private void DismissInvalidInvitations()\n        {\n            var toDismiss = new List<UserChannelPair>();\n\n            foreach (KeyValuePair<UserChannelPair, WeakReference> invitation in invitationIndex)\n            {\n                var gameIndex =\n                    lbGameList.HostedGames.FindIndex(hg =>\n                    ((HostedCnCNetGame)hg).HostName == invitation.Key.Item1 &&\n                    ((HostedCnCNetGame)hg).ChannelName == invitation.Key.Item2);\n\n                if (gameIndex == -1)\n                {\n                    toDismiss.Add(invitation.Key);\n                }\n            }\n\n            foreach (UserChannelPair invitationIdentity in toDismiss)\n            {\n                DismissInvitation(invitationIdentity);\n            }\n        }\n\n        private void DismissInvitation(UserChannelPair invitationIdentity)\n        {\n            if (invitationIndex.ContainsKey(invitationIdentity))\n            {\n                var invitationNotification = invitationIndex[invitationIdentity].Target as ChoiceNotificationBox;\n\n                if (invitationNotification != null)\n                {\n                    WindowManager.RemoveControl(invitationNotification);\n                }\n\n                invitationIndex.Remove(invitationIdentity);\n            }\n        }\n\n        /// <summary>\n        /// Attempts to find a hosted game that the specified user is in\n        /// </summary>\n        /// <param name=\"user\">The user to find a game for.</param>\n        /// <returns></returns>\n        private HostedCnCNetGame GetHostedGameForUser(IRCUser user)\n        {\n            return lbGameList.HostedGames.Select(g => (HostedCnCNetGame)g).FirstOrDefault(g => g.Players.Contains(user.Name));\n        }\n\n        /// <summary>\n        /// Joins a specified user's game depending on whether or not\n        /// they are currently in one.\n        /// </summary>\n        /// <param name=\"user\">The user to join.</param>\n        /// <param name=\"messageView\">The message view/list to write error messages to.</param>\n        private void JoinUser(IRCUser user, IMessageView messageView)\n        {\n            if (user == null)\n            {\n                // can happen if a user is selected while offline\n                messageView.AddMessage(new ChatMessage(Color.White, \"User is not currently available!\".L10N(\"Client:Main:UserNotAvailable\")));\n                return;\n            }\n            var game = GetHostedGameForUser(user);\n            if (game == null)\n            {\n                messageView.AddMessage(new ChatMessage(Color.White, string.Format(\"{0} is not in a game!\".L10N(\"Client:Main:UserNotInGame\"), user.Name)));\n                return;\n            }\n\n            int gameIndex = lbGameList.Items.FindIndex(item => item.Tag == game);\n            if (gameIndex >= 0)\n            {\n                lbGameList.SelectedIndex = gameIndex;\n                lbGameList.ScrollToSelectedElement();\n            }\n\n            JoinGame(game, string.Empty, messageView);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLoginWindow.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    class CnCNetLoginWindow : XNAWindow\n    {\n        public CnCNetLoginWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        XNALabel lblConnectToCnCNet;\n        XNATextBox tbPlayerName;\n        XNALabel lblPlayerName;\n        XNAClientCheckBox chkRememberMe;\n        XNAClientCheckBox chkPersistentMode;\n        XNAClientCheckBox chkAutoConnect;\n        XNAClientButton btnConnect;\n        XNAClientButton btnCancel;\n\n        public event EventHandler Cancelled;\n        public event EventHandler Connect;\n\n        public override void Initialize()\n        {\n            Name = \"CnCNetLoginWindow\";\n            ClientRectangle = new Rectangle(0, 0, 300, 220);\n            BackgroundTexture = AssetLoader.LoadTextureUncached(\"logindialogbg.png\");\n\n            lblConnectToCnCNet = new XNALabel(WindowManager);\n            lblConnectToCnCNet.Name = \"lblConnectToCnCNet\";\n            lblConnectToCnCNet.FontIndex = 1;\n            lblConnectToCnCNet.Text = \"CONNECT TO CNCNET\".L10N(\"Client:Main:ConnectToCncNet\");\n\n            AddChild(lblConnectToCnCNet);\n            lblConnectToCnCNet.CenterOnParent();\n            lblConnectToCnCNet.ClientRectangle = new Rectangle(\n                lblConnectToCnCNet.X, 12,\n                lblConnectToCnCNet.Width, \n                lblConnectToCnCNet.Height);\n\n            tbPlayerName = new XNATextBox(WindowManager);\n            tbPlayerName.Name = \"tbPlayerName\";\n            tbPlayerName.ClientRectangle = new Rectangle(Width - 132, 50, 120, 19);\n            tbPlayerName.MaximumTextLength = ClientConfiguration.Instance.MaxNameLength;\n            tbPlayerName.IMEDisabled = true;\n            string defgame = ClientConfiguration.Instance.LocalGame;\n\n            lblPlayerName = new XNALabel(WindowManager);\n            lblPlayerName.Name = \"lblPlayerName\";\n            lblPlayerName.FontIndex = 1;\n            lblPlayerName.Text = \"PLAYER NAME:\".L10N(\"Client:Main:PlayerName\");\n            lblPlayerName.ClientRectangle = new Rectangle(12, tbPlayerName.Y + 1,\n                lblPlayerName.Width, lblPlayerName.Height);\n\n            chkRememberMe = new XNAClientCheckBox(WindowManager);\n            chkRememberMe.Name = \"chkRememberMe\";\n            chkRememberMe.ClientRectangle = new Rectangle(12, tbPlayerName.Bottom + 12, 0, 0);\n            chkRememberMe.Text = \"Remember me\".L10N(\"Client:Main:RememberMe\");\n            chkRememberMe.TextPadding = 7;\n            chkRememberMe.CheckedChanged += ChkRememberMe_CheckedChanged;\n\n            chkPersistentMode = new XNAClientCheckBox(WindowManager);\n            chkPersistentMode.Name = \"chkPersistentMode\";\n            chkPersistentMode.ClientRectangle = new Rectangle(12, chkRememberMe.Bottom + 30, 0, 0);\n            chkPersistentMode.Text = \"Stay connected outside of the CnCNet lobby\".L10N(\"Client:Main:StayConnect\");\n            chkPersistentMode.TextPadding = chkRememberMe.TextPadding;\n            chkPersistentMode.CheckedChanged += ChkPersistentMode_CheckedChanged;\n\n            chkAutoConnect = new XNAClientCheckBox(WindowManager);\n            chkAutoConnect.Name = \"chkAutoConnect\";\n            chkAutoConnect.ClientRectangle = new Rectangle(12, chkPersistentMode.Bottom + 30, 0, 0);\n            chkAutoConnect.Text = \"Connect automatically on client startup\".L10N(\"Client:Main:AutoConnect\");\n            chkAutoConnect.TextPadding = chkRememberMe.TextPadding;\n            chkAutoConnect.AllowChecking = false;\n\n            btnConnect = new XNAClientButton(WindowManager);\n            btnConnect.Name = \"btnConnect\";\n            btnConnect.ClientRectangle = new Rectangle(12, Height - 35, 110, 23);\n            btnConnect.Text = \"Connect\".L10N(\"Client:Main:ButtonConnect\");\n            btnConnect.LeftClick += BtnConnect_LeftClick;\n\n            btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = \"btnCancel\";\n            btnCancel.ClientRectangle = new Rectangle(Width - 122, btnConnect.Y, 110, 23);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(tbPlayerName);\n            AddChild(lblPlayerName);\n            AddChild(chkRememberMe);\n            AddChild(chkPersistentMode);\n            AddChild(chkAutoConnect);\n            AddChild(btnConnect);\n            AddChild(btnCancel);\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved;\n        }\n\n        private void Instance_SettingsSaved(object sender, EventArgs e)\n        {\n            tbPlayerName.Text = UserINISettings.Instance.PlayerName;\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Cancelled?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void ChkRememberMe_CheckedChanged(object sender, EventArgs e)\n        {\n            CheckAutoConnectAllowance();\n        }\n\n        private void ChkPersistentMode_CheckedChanged(object sender, EventArgs e)\n        {\n            CheckAutoConnectAllowance();\n        }\n\n        private void CheckAutoConnectAllowance()\n        {\n            chkAutoConnect.AllowChecking = chkPersistentMode.Checked && chkRememberMe.Checked;\n            if (!chkAutoConnect.AllowChecking)\n                chkAutoConnect.Checked = false;\n        }\n\n        private void BtnConnect_LeftClick(object sender, EventArgs e)\n        {\n            NameValidationError validationError = NameValidator.IsNameValid(tbPlayerName.Text, out string errorMessage);\n\n            if (validationError != NameValidationError.None)\n            {\n                XNAMessageBox.Show(WindowManager, \"Invalid Player Name\".L10N(\"Client:Main:InvalidPlayerName\"), errorMessage);\n                return;\n            }\n\n            ProgramConstants.PLAYERNAME = tbPlayerName.Text;\n\n            UserINISettings.Instance.SkipConnectDialog.Value = chkRememberMe.Checked;\n            UserINISettings.Instance.PersistentMode.Value = chkPersistentMode.Checked;\n            UserINISettings.Instance.AutomaticCnCNetLogin.Value = chkAutoConnect.Checked;\n            UserINISettings.Instance.PlayerName.Value = ProgramConstants.PLAYERNAME;\n\n            UserINISettings.Instance.SaveSettings();\n\n            Connect?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void LoadSettings()\n        {\n            chkAutoConnect.Checked = UserINISettings.Instance.AutomaticCnCNetLogin;\n            chkPersistentMode.Checked = UserINISettings.Instance.PersistentMode;\n            chkRememberMe.Checked = UserINISettings.Instance.SkipConnectDialog;\n\n            tbPlayerName.Text = UserINISettings.Instance.PlayerName;\n\n            if (chkRememberMe.Checked)\n                BtnConnect_LeftClick(this, EventArgs.Empty);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationEventArgs.cs",
    "content": "﻿using DTAClient.Domain.Multiplayer.CnCNet;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    class GameCreationEventArgs : EventArgs\n    {\n        public GameCreationEventArgs(string roomName, int maxPlayers, \n            string password, CnCNetTunnel tunnel, int skillLevel)\n        {\n            GameRoomName = roomName;\n            MaxPlayers = maxPlayers;\n            Password = password;\n            Tunnel = tunnel;\n            SkillLevel = skillLevel;\n        }\n\n        public string GameRoomName { get; private set; }\n        public int MaxPlayers { get; private set; }\n        public string Password { get; private set; }\n        public CnCNetTunnel Tunnel { get; private set; }\n        public int SkillLevel { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs",
    "content": "using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.IO;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A window that allows the user to host a new game on CnCNet.\n    /// </summary>\n    class GameCreationWindow : XNAWindow\n    {\n        public GameCreationWindow(WindowManager windowManager, TunnelHandler tunnelHandler)\n            : base(windowManager)\n        {\n            this.tunnelHandler = tunnelHandler;\n        }\n\n        public event EventHandler Cancelled;\n        public event EventHandler<GameCreationEventArgs> GameCreated;\n        public event EventHandler<GameCreationEventArgs> LoadedGameCreated;\n\n        private XNATextBox tbGameName;\n        private XNAClientDropDown ddMaxPlayers;\n        private XNAClientDropDown ddSkillLevel;\n        private XNATextBox tbPassword;\n\n        private XNALabel lblRoomName;\n        private XNALabel lblMaxPlayers;\n        private XNALabel lblSkillLevel;\n        private XNALabel lblPassword;\n\n        private XNALabel lblTunnelServer;\n        private TunnelListBox lbTunnelList;\n\n        private XNAClientButton btnCreateGame;\n        private XNAClientButton btnCancel;\n        private XNAClientButton btnLoadMPGame;\n        private XNAClientButton btnDisplayAdvancedOptions;\n\n        private TunnelHandler tunnelHandler;\n\n        private string[] SkillLevelOptions;\n\n        public override void Initialize()\n        {\n            lbTunnelList = new TunnelListBox(WindowManager, tunnelHandler);\n            lbTunnelList.Name = nameof(lbTunnelList);\n\n            SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n\n            Name = \"GameCreationWindow\";\n            Width = lbTunnelList.Width + UIDesignConstants.EMPTY_SPACE_SIDES * 2 +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN * 2;\n            BackgroundTexture = AssetLoader.LoadTexture(\"gamecreationoptionsbg.png\");\n\n            tbGameName = new XNATextBox(WindowManager);\n            tbGameName.Name = nameof(tbGameName);\n            tbGameName.MaximumTextLength = 23;\n            tbGameName.ClientRectangle = new Rectangle(Width - 150 - UIDesignConstants.EMPTY_SPACE_SIDES -\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, UIDesignConstants.EMPTY_SPACE_TOP +\n                UIDesignConstants.CONTROL_VERTICAL_MARGIN, 150, 21);\n            tbGameName.Text = string.Format(\"{0}'s Game\", ProgramConstants.PLAYERNAME);\n\n            lblRoomName = new XNALabel(WindowManager);\n            lblRoomName.Name = nameof(lblRoomName);\n            lblRoomName.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, tbGameName.Y + 1, 0, 0);\n            lblRoomName.Text = \"Game room name:\".L10N(\"Client:Main:GameRoomName\");\n\n            ddMaxPlayers = new XNAClientDropDown(WindowManager);\n            ddMaxPlayers.Name = nameof(ddMaxPlayers);\n            ddMaxPlayers.ClientRectangle = new Rectangle(tbGameName.X, tbGameName.Bottom + 20,\n                tbGameName.Width, 21);\n            for (int i = 8; i > 1; i--)\n                ddMaxPlayers.AddItem(i.ToString());\n            ddMaxPlayers.SelectedIndex = 0;\n\n            lblMaxPlayers = new XNALabel(WindowManager);\n            lblMaxPlayers.Name = nameof(lblMaxPlayers);\n            lblMaxPlayers.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, ddMaxPlayers.Y + 1, 0, 0);\n            lblMaxPlayers.Text = \"Maximum number of players:\".L10N(\"Client:Main:GameMaxPlayerCount\");\n\n            // Skill Level selector\n            ddSkillLevel = new XNAClientDropDown(WindowManager);\n            ddSkillLevel.Name = nameof(ddSkillLevel);\n            ddSkillLevel.ClientRectangle = new Rectangle(tbGameName.X, ddMaxPlayers.Bottom + 20,\n                tbGameName.Width, 21);\n\n            for (int i = 0; i < SkillLevelOptions.Length; i++)\n            {\n                string skillLevel = SkillLevelOptions[i];\n                string localizedSkillLevel = skillLevel.L10N($\"INI:ClientDefinitions:SkillLevel:{i}\");\n                ddSkillLevel.AddItem(localizedSkillLevel);\n            }\n\n            ddSkillLevel.SelectedIndex = ClientConfiguration.Instance.DefaultSkillLevelIndex;\n\n            lblSkillLevel = new XNALabel(WindowManager);\n            lblSkillLevel.Name = nameof(lblSkillLevel);\n            lblSkillLevel.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, ddSkillLevel.Y + 1, 0, 0);\n            lblSkillLevel.Text = \"Select preferred skill level of players:\".L10N(\"Client:Main:SelectSkillLevel\");\n\n            tbPassword = new XNATextBox(WindowManager);\n            tbPassword.Name = nameof(tbPassword);\n            tbPassword.MaximumTextLength = 20;\n            tbPassword.ClientRectangle = new Rectangle(tbGameName.X, ddSkillLevel.Bottom + 20,\n                tbGameName.Width, 21);\n\n            lblPassword = new XNALabel(WindowManager);\n            lblPassword.Name = nameof(lblPassword);\n            lblPassword.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, tbPassword.Y + 1, 0, 0);\n            lblPassword.Text = \"Password (leave blank for none):\".L10N(\"Client:Main:PasswordTextBlankForNone\");\n\n            btnDisplayAdvancedOptions = new XNAClientButton(WindowManager);\n            btnDisplayAdvancedOptions.Name = nameof(btnDisplayAdvancedOptions);\n            btnDisplayAdvancedOptions.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblPassword.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnDisplayAdvancedOptions.Text = \"Advanced Options\".L10N(\"Client:Main:AdvancedOptions\");\n            btnDisplayAdvancedOptions.LeftClick += BtnDisplayAdvancedOptions_LeftClick;\n\n            lblTunnelServer = new XNALabel(WindowManager);\n            lblTunnelServer.Name = nameof(lblTunnelServer);\n            lblTunnelServer.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblPassword.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 4, 0, 0);\n            lblTunnelServer.Text = \"Tunnel server:\".L10N(\"Client:Main:TunnelServer\");\n            lblTunnelServer.Enabled = false;\n            lblTunnelServer.Visible = false;\n\n            lbTunnelList.X = UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            lbTunnelList.Y = lblTunnelServer.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN;\n            lbTunnelList.Disable();\n            lbTunnelList.ListRefreshed += LbTunnelList_ListRefreshed;\n\n            btnCreateGame = new XNAClientButton(WindowManager);\n            btnCreateGame.Name = nameof(btnCreateGame);\n            btnCreateGame.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnDisplayAdvancedOptions.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3,\n                UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnCreateGame.Text = \"Create Game\".L10N(\"Client:Main:CreateGame\");\n            btnCreateGame.LeftClick += BtnCreateGame_LeftClick;\n\n            btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.ClientRectangle = new Rectangle(Width - UIDesignConstants.BUTTON_WIDTH_133 - UIDesignConstants.EMPTY_SPACE_SIDES -\n                UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnCreateGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            int btnLoadMPGameX = btnCreateGame.Right + (btnCancel.X - btnCreateGame.Right) / 2 - UIDesignConstants.BUTTON_WIDTH_133 / 2;\n\n            btnLoadMPGame = new XNAClientButton(WindowManager);\n            btnLoadMPGame.Name = nameof(btnLoadMPGame);\n            btnLoadMPGame.ClientRectangle = new Rectangle(btnLoadMPGameX, btnCreateGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLoadMPGame.Text = \"Load Game\".L10N(\"Client:Main:LoadGame\");\n            btnLoadMPGame.LeftClick += BtnLoadMPGame_LeftClick;\n\n            AddChild(tbGameName);\n            AddChild(lblRoomName);\n            AddChild(ddMaxPlayers);\n            AddChild(lblMaxPlayers);\n            AddChild(ddSkillLevel);\n            AddChild(lblSkillLevel);\n            AddChild(tbPassword);\n            AddChild(lblPassword);\n            AddChild(btnDisplayAdvancedOptions);\n            AddChild(lblTunnelServer);\n            AddChild(lbTunnelList);\n            AddChild(btnCreateGame);\n            if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading)\n                AddChild(btnLoadMPGame);\n            AddChild(btnCancel);\n\n            Height = btnCreateGame.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved;\n\n            if (UserINISettings.Instance.AlwaysDisplayTunnelList)\n                BtnDisplayAdvancedOptions_LeftClick(this, EventArgs.Empty);\n        }\n\n        private void LbTunnelList_ListRefreshed(object sender, EventArgs e)\n        {\n            if (lbTunnelList.ItemCount == 0)\n            {\n                btnCreateGame.AllowClick = false;\n                btnLoadMPGame.AllowClick = false;\n            }\n            else\n            {\n                btnCreateGame.AllowClick = true;\n                btnLoadMPGame.AllowClick = AllowLoadingGame();\n            }\n        }\n\n        private void Instance_SettingsSaved(object sender, EventArgs e)\n        {\n            tbGameName.Text = string.Format(\"{0}'s Game\", UserINISettings.Instance.PlayerName.Value);\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Cancelled?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void BtnLoadMPGame_LeftClick(object sender, EventArgs e)\n        {\n            string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text);\n\n            NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage);\n            if (validationError != NameValidationError.None)\n            {\n                XNAMessageBox.Show(WindowManager, \"Invalid game name\".L10N(\"Client:Main:InvalidGameName\"),\n                    errorMessage);\n                return;\n            }\n\n            if (!lbTunnelList.IsValidIndexSelected())\n                return;\n\n            IniFile spawnSGIni =\n                new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI));\n\n            string password = Utilities.CalculateSHA1ForString(\n                spawnSGIni.GetStringValue(\"Settings\", \"GameID\", string.Empty)).Substring(0, 10);\n\n            GameCreationEventArgs ea = new GameCreationEventArgs(gameName,\n                spawnSGIni.GetIntValue(\"Settings\", \"PlayerCount\", 2), password,\n                tunnelHandler.Tunnels[lbTunnelList.SelectedIndex], ddSkillLevel.SelectedIndex);\n\n            LoadedGameCreated?.Invoke(this, ea);\n        }\n\n        private void BtnCreateGame_LeftClick(object sender, EventArgs e)\n        {\n            string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text);\n\n            NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage);\n            if (validationError != NameValidationError.None)\n            {\n                XNAMessageBox.Show(WindowManager, \"Invalid game name\".L10N(\"Client:Main:InvalidGameName\"),\n                    errorMessage);\n                return;\n            }\n\n            if (!lbTunnelList.IsValidIndexSelected())\n            {\n                return;\n            }\n\n            GameCreated?.Invoke(this,\n                new GameCreationEventArgs(gameName,int.Parse(ddMaxPlayers.SelectedItem.Text),\n                tbPassword.Text,tunnelHandler.Tunnels[lbTunnelList.SelectedIndex],\n                ddSkillLevel.SelectedIndex)\n            );\n        }\n\n        private void BtnDisplayAdvancedOptions_LeftClick(object sender, EventArgs e)\n        {\n            Name = \"GameCreationWindow_Advanced\";\n\n            btnCreateGame.ClientRectangle = new Rectangle(btnCreateGame.X,\n                lbTunnelList.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3,\n                btnCreateGame.Width, btnCreateGame.Height);\n\n            btnCancel.ClientRectangle = new Rectangle(btnCancel.X,\n                btnCreateGame.Y, btnCancel.Width, btnCancel.Height);\n\n            btnLoadMPGame.ClientRectangle = new Rectangle(btnLoadMPGame.X,\n                btnCreateGame.Y, btnLoadMPGame.Width, btnLoadMPGame.Height);\n\n            Height = btnCreateGame.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n\n            lblTunnelServer.Enable();\n            lbTunnelList.Enable();\n            btnDisplayAdvancedOptions.Disable();\n\n            SetAttributesFromIni();\n\n            CenterOnParent();\n        }\n\n        public void Refresh()\n        {\n            btnLoadMPGame.AllowClick = AllowLoadingGame();\n        }\n\n        private bool AllowLoadingGame()\n        {\n            FileInfo savedGameSpawnIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI);\n\n            if (!savedGameSpawnIniFile.Exists)\n                return false;\n\n            IniFile iniFile = new IniFile(savedGameSpawnIniFile.FullName);\n\n            if (iniFile.GetStringValue(\"Settings\", \"Name\", string.Empty) != ProgramConstants.PLAYERNAME)\n                return false;\n\n            if (!iniFile.GetBooleanValue(\"Settings\", \"Host\", false))\n                return false;\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\n\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.Online;\nusing DTAClient.Online.EventArguments;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nusing TextCopy;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class GlobalContextMenu : XNAContextMenu\n    {\n        private readonly string PRIVATE_MESSAGE = \"Private Message\".L10N(\"Client:Main:PrivateMessage\");\n        private readonly string ADD_FRIEND = \"Add Friend\".L10N(\"Client:Main:AddFriend\");\n        private readonly string REMOVE_FRIEND = \"Remove Friend\".L10N(\"Client:Main:RemoveFriend\");\n        private readonly string BLOCK = \"Block\".L10N(\"Client:Main:Block\");\n        private readonly string UNBLOCK = \"Unblock\".L10N(\"Client:Main:Unblock\");\n        private readonly string INVITE = \"Invite\".L10N(\"Client:Main:Invite\");\n        private readonly string JOIN = \"Join\".L10N(\"Client:Main:Join\");\n        private readonly string COPY_LINK = \"Copy Link\".L10N(\"Client:Main:CopyLink\");\n        private readonly string OPEN_LINK = \"Open Link\".L10N(\"Client:Main:OpenLink\");\n\n        private readonly int SHORT_LINK_MINIMAL_LENGTH = 40;\n        private readonly int SHORT_LINK_PREFIX_LENGTH = 30;\n        private readonly int SHORT_LINK_SUFFIX_LENGTH = 5;\n\n        private readonly Rectangle STD_SIZE = new Rectangle(0, 0, 150, 2);\n        private readonly Rectangle LNK_SIZE = new Rectangle(0, 0, 300, 2);\n\n        private readonly CnCNetUserData cncnetUserData;\n        private readonly PrivateMessagingWindow pmWindow;\n        private XNAContextMenuItem privateMessageItem;\n        private XNAContextMenuItem toggleFriendItem;\n        private XNAContextMenuItem toggleIgnoreItem;\n        private XNAContextMenuItem invitePlayerItem;\n        private XNAContextMenuItem joinPlayerItem;\n\n        protected readonly CnCNetManager connectionManager;\n        protected GlobalContextMenuData contextMenuData;\n\n        public EventHandler<JoinUserEventArgs> JoinEvent;\n\n        private IReadOnlyList<XNAContextMenuItem> DefaultMenuItems = [];\n\n        public GlobalContextMenu(\n            WindowManager windowManager,\n            CnCNetManager connectionManager,\n            CnCNetUserData cncnetUserData,\n            PrivateMessagingWindow pmWindow\n        ) : base(windowManager)\n        {\n            this.connectionManager = connectionManager;\n            this.cncnetUserData = cncnetUserData;\n            this.pmWindow = pmWindow;\n\n            Name = nameof(GlobalContextMenu);\n            ClientRectangle = STD_SIZE;\n            Enabled = false;\n            Visible = false;\n        }\n\n        public override void Initialize()\n        {\n            privateMessageItem = new XNAContextMenuItem()\n            {\n                Text = PRIVATE_MESSAGE,\n                SelectAction = () => pmWindow.InitPM(GetIrcUser().Name)\n            };\n            toggleFriendItem = new XNAContextMenuItem()\n            {\n                Text = ADD_FRIEND,\n                SelectAction = () => cncnetUserData.ToggleFriend(GetIrcUser().Name)\n            };\n            toggleIgnoreItem = new XNAContextMenuItem()\n            {\n                Text = BLOCK,\n                SelectAction = () => GetIrcUserIdent(cncnetUserData.ToggleIgnoreUser)\n            };\n            invitePlayerItem = new XNAContextMenuItem()\n            {\n                Text = INVITE,\n                SelectAction = Invite\n            };\n            joinPlayerItem = new XNAContextMenuItem()\n            {\n                Text = JOIN,\n                SelectAction = () => JoinEvent?.Invoke(this, new JoinUserEventArgs(GetIrcUser()))\n            };\n\n            DefaultMenuItems = [privateMessageItem, toggleFriendItem, toggleIgnoreItem, invitePlayerItem, joinPlayerItem];\n\n            foreach (var item in DefaultMenuItems)\n                AddItem(item);\n        }\n\n        private void Invite()\n        {\n            // note it's assumed that if the channel name is specified, the game name must be also\n            if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame)\n            {\n                return;\n            }\n\n            string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + \" \" + contextMenuData.inviteChannelName + \";\" + contextMenuData.inviteGameName;\n\n            if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword))\n            {\n                messageBody += \";\" + contextMenuData.inviteChannelPassword;\n            }\n\n            connectionManager.SendCustomMessage(new QueuedMessage(\n                \"PRIVMSG \" + GetIrcUser().Name + \" :\\u0001\" + messageBody + \"\\u0001\", QueuedMessageType.CHAT_MESSAGE, 0\n            ));\n        }\n\n        private void UpdateButtons()\n        {\n            UpdatePlayerBasedButtons();\n            UpdateMessageBasedButtons();\n        }\n\n        private void UpdatePlayerBasedButtons()\n        {\n            var ircUser = GetIrcUser();\n            var isOnline = ircUser != null && connectionManager.UserList.Any(u => u.Name == ircUser.Name);\n            var isAdmin = contextMenuData.ChannelUser?.IsAdmin ?? false;\n\n            toggleFriendItem.Visible = ircUser != null;\n            privateMessageItem.Visible = ircUser != null && isOnline;\n            toggleIgnoreItem.Visible = ircUser != null;\n            invitePlayerItem.Visible = ircUser != null && isOnline && !string.IsNullOrEmpty(contextMenuData.inviteChannelName);\n            joinPlayerItem.Visible = ircUser != null && !contextMenuData.PreventJoinGame && isOnline;\n\n            toggleIgnoreItem.Selectable = !isAdmin;\n\n            if (ircUser == null)\n                return;\n\n            toggleFriendItem.Text = cncnetUserData.IsFriend(ircUser.Name) ? REMOVE_FRIEND : ADD_FRIEND;\n            toggleIgnoreItem.Text = cncnetUserData.IsIgnored(ircUser.Ident) ? UNBLOCK : BLOCK;\n        }\n\n        private void UpdateMessageBasedButtons()\n        {\n            Items = DefaultMenuItems.ToList();\n\n            var links = contextMenuData?.ChatMessage?.Message?.GetLinks();\n\n            if (links == null)\n            {\n                ClientRectangle = STD_SIZE;\n                return;\n            }\n\n            ClientRectangle = LNK_SIZE;\n\n            foreach (string link in links)\n            {\n                // Shorten the links if it's too long\n                string linkToDisplay = link;\n                if (link.Length > SHORT_LINK_MINIMAL_LENGTH)\n                    linkToDisplay = link[..SHORT_LINK_PREFIX_LENGTH] + \"...\" + link[^SHORT_LINK_SUFFIX_LENGTH..];\n\n                if (Items.Where(item => item.Text.Contains(linkToDisplay)).ToList().Count > 0)\n                    continue;\n\n                var copyLinkItem = new XNAContextMenuItem()\n                {\n                    Text = $\"{COPY_LINK} {linkToDisplay}\",\n                    SelectAction = () => CopyLink(link)\n                };\n\n                var openLinkItem = new XNAContextMenuItem()\n                {\n                    Text = $\"{OPEN_LINK} {linkToDisplay}\",\n                    SelectAction = () => URLHandler.OpenLink(WindowManager, link)\n                };\n\n                AddItem(openLinkItem);\n                AddItem(copyLinkItem);\n            }\n        }\n\n        private void CopyLink(string link)\n        {\n            try\n            {\n                ClipboardService.SetText(link);\n            }\n            catch (Exception)\n            {\n                XNAMessageBox.Show(WindowManager, \"Error\".L10N(\"Client:Main:Error\"), \"Unable to copy link\".L10N(\"Client:Main:ClipboardCopyLinkFailed\"));\n            }\n        }\n\n        private void GetIrcUserIdent(Action<string> callback)\n        {\n            var ircUser = GetIrcUser();\n\n            if (!string.IsNullOrEmpty(ircUser.Ident))\n            {\n                callback.Invoke(ircUser.Ident);\n                return;\n            }\n\n            void WhoIsReply(object sender, WhoEventArgs whoEventargs)\n            {\n                ircUser.Ident = whoEventargs.Ident;\n                callback.Invoke(whoEventargs.Ident);\n                connectionManager.WhoReplyReceived -= WhoIsReply;\n            }\n\n            connectionManager.WhoReplyReceived += WhoIsReply;\n            connectionManager.SendWhoIsMessage(ircUser.Name);\n        }\n\n        private IRCUser GetIrcUser()\n        {\n            if (contextMenuData.IrcUser != null)\n                return contextMenuData.IrcUser;\n\n            if (contextMenuData.ChannelUser?.IRCUser != null)\n                return contextMenuData.ChannelUser.IRCUser;\n\n            if (!string.IsNullOrEmpty(contextMenuData.PlayerName))\n                return connectionManager.UserList.Find(u => u.Name == contextMenuData.PlayerName);\n\n            if (!string.IsNullOrEmpty(contextMenuData.ChatMessage?.SenderName))\n                return connectionManager.UserList.Find(u => u.Name == contextMenuData.ChatMessage.SenderName);\n\n            return null;\n        }\n\n        public void Show(string playerName, Point cursorPoint)\n        {\n            Show(new GlobalContextMenuData\n            {\n                PlayerName = playerName\n            }, cursorPoint);\n        }\n\n        public void Show(IRCUser ircUser, Point cursorPoint)\n        {\n            Show(new GlobalContextMenuData\n            {\n                IrcUser = ircUser\n            }, cursorPoint);\n        }\n\n        public void Show(ChannelUser channelUser, Point cursorPoint)\n        {\n            Show(new GlobalContextMenuData\n            {\n                ChannelUser = channelUser\n            }, cursorPoint);\n        }\n\n        public void Show(ChatMessage chatMessage, Point cursorPoint)\n        {\n            Show(new GlobalContextMenuData()\n            {\n                ChatMessage = chatMessage\n            }, cursorPoint);\n        }\n\n        public void Show(GlobalContextMenuData data, Point cursorPoint)\n        {\n            Disable();\n            contextMenuData = data;\n            UpdateButtons();\n\n            if (!Items.Any(i => i.Visible))\n                return;\n\n            Open(cursorPoint);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenuData.cs",
    "content": "﻿using DTAClient.Online;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class GlobalContextMenuData\n    {\n        /// <summary>\n        /// The ChannelUser to show the menu for.\n        /// </summary>\n        public ChannelUser ChannelUser { get; set; }\n        \n        /// <summary>\n        /// The ChatMessage to show the menu for.\n        /// </summary>\n        public ChatMessage ChatMessage { get; set; }\n        \n        /// <summary>\n        /// The IRCUser to show the menu for.\n        /// </summary>\n        public IRCUser IrcUser { get; set; }\n        \n        /// <summary>\n        /// The player to show the menu for. This is used to determine the IRCUser internally.\n        /// </summary>\n        public string PlayerName { get; set; }\n        \n        /// <summary>\n        /// The invite properties are used for the Invite option in the menu.\n        /// </summary>\n        public string inviteChannelName { get; set; }\n        public string inviteGameName { get; set; }\n        public string inviteChannelPassword { get; set; }\n        \n        /// <summary>\n        /// Prevent the Join option from showing in the menu.\n        /// </summary>\n        public bool PreventJoinGame { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Online.EventArguments;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class LoadOrSaveGameOptionPresetWindow : XNAWindow\n    {\n        private bool _isLoad;\n\n        private readonly XNALabel lblHeader;\n\n        private readonly XNADropDownItem ddiCreatePresetItem;\n\n        private readonly XNADropDownItem ddiSelectPresetItem;\n\n        private readonly XNAClientButton btnLoadSave;\n\n        private readonly XNAClientButton btnDelete;\n\n        private readonly XNAClientDropDown ddPresetSelect;\n\n        private readonly XNALabel lblNewPresetName;\n\n        private readonly XNATextBox tbNewPresetName;\n\n        public EventHandler<GameOptionPresetEventArgs> PresetLoaded;\n\n        public EventHandler<GameOptionPresetEventArgs> PresetSaved;\n\n        public LoadOrSaveGameOptionPresetWindow(WindowManager windowManager) : base(windowManager)\n        {\n            ClientRectangle = new Rectangle(0, 0, 325, 185);\n\n            var margin = 10;\n\n            lblHeader = new XNALabel(WindowManager);\n            lblHeader.Name = nameof(lblHeader);\n            lblHeader.FontIndex = 1;\n            lblHeader.ClientRectangle = new Rectangle(\n                margin, margin,\n                150, 22\n            );\n\n            var lblPresetName = new XNALabel(WindowManager);\n            lblPresetName.Name = nameof(lblPresetName);\n            lblPresetName.Text = \"Preset Name\".L10N(\"Client:Main:PresetName\");\n            lblPresetName.ClientRectangle = new Rectangle(\n                margin, lblHeader.Bottom + margin,\n                150, 18\n            );\n\n            ddiCreatePresetItem = new XNADropDownItem();\n            ddiCreatePresetItem.Text = \"[Create New]\".L10N(\"Client:Main:CreateNewPreset\");\n\n            ddiSelectPresetItem = new XNADropDownItem();\n            ddiSelectPresetItem.Text = \"[Select Preset]\".L10N(\"Client:Main:SelectPreset\");\n            ddiSelectPresetItem.Selectable = false;\n\n            ddPresetSelect = new XNAClientDropDown(WindowManager);\n            ddPresetSelect.Name = nameof(ddPresetSelect);\n            ddPresetSelect.ClientRectangle = new Rectangle(\n                10, lblPresetName.Bottom + 2,\n                150, 22\n            );\n            ddPresetSelect.SelectedIndexChanged += DropDownPresetSelect_SelectedIndexChanged;\n\n            lblNewPresetName = new XNALabel(WindowManager);\n            lblNewPresetName.Name = nameof(lblNewPresetName);\n            lblNewPresetName.Text = \"New Preset Name\".L10N(\"Client:Main:NewPresetName\");\n            lblNewPresetName.ClientRectangle = new Rectangle(\n                margin, ddPresetSelect.Bottom + margin,\n                150, 18\n            );\n\n            tbNewPresetName = new XNATextBox(WindowManager);\n            tbNewPresetName.Name = nameof(tbNewPresetName);\n            tbNewPresetName.ClientRectangle = new Rectangle(\n                10, lblNewPresetName.Bottom + 2,\n                150, 22\n            );\n            tbNewPresetName.TextChanged += (sender, args) => RefreshButtons();\n\n            btnLoadSave = new XNAClientButton(WindowManager);\n            btnLoadSave.Name = nameof(btnLoadSave);\n            btnLoadSave.LeftClick += BtnLoadSave_LeftClick;\n            btnLoadSave.ClientRectangle = new Rectangle(\n                margin,\n                Height - UIDesignConstants.BUTTON_HEIGHT - margin,\n                UIDesignConstants.BUTTON_WIDTH_92,\n                UIDesignConstants.BUTTON_HEIGHT\n            );\n\n            btnDelete = new XNAClientButton(WindowManager);\n            btnDelete.Name = nameof(btnDelete);\n            btnDelete.Text = \"Delete\".L10N(\"Client:Main:ButtonDelete\");\n            btnDelete.LeftClick += BtnDelete_LeftClick;\n            btnDelete.ClientRectangle = new Rectangle(\n                btnLoadSave.Right + margin,\n                btnLoadSave.Y,\n                UIDesignConstants.BUTTON_WIDTH_92,\n                UIDesignConstants.BUTTON_HEIGHT\n            );\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.ClientRectangle = new Rectangle(\n                btnDelete.Right + margin,\n                btnLoadSave.Y,\n                UIDesignConstants.BUTTON_WIDTH_92,\n                UIDesignConstants.BUTTON_HEIGHT\n            );\n            btnCancel.LeftClick += (sender, args) => Disable();\n\n            AddChild(lblHeader);\n            AddChild(lblPresetName);\n            AddChild(ddPresetSelect);\n            AddChild(lblNewPresetName);\n            AddChild(tbNewPresetName);\n            AddChild(btnLoadSave);\n            AddChild(btnDelete);\n            AddChild(btnCancel);\n\n            Disable();\n        }\n\n        public override void Initialize()\n        {\n            Name = \"LoadOrSaveGameOptionPresetWindow\";\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1);\n\n            base.Initialize();\n        }\n\n        /// <summary>\n        /// Show the window.\n        /// </summary>\n        /// <param name=\"isLoad\">The \"mode\" for the window: load vs save.</param>\n        public void Show(bool isLoad)\n        {\n            _isLoad = isLoad;\n            lblHeader.Text = _isLoad ? \"Load Preset\".L10N(\"Client:Main:LoadPreset\") : \"Save Preset\".L10N(\"Client:Main:SavePreset\");\n            btnLoadSave.Text = _isLoad ? \"Load\".L10N(\"Client:Main:ButtonLoad\") : \"Save\".L10N(\"Client:Main:ButtonSave\");\n\n            if (_isLoad)\n                ShowLoad();\n            else\n                ShowSave();\n\n            RefreshButtons();\n            CenterOnParent();\n            Enable();\n        }\n\n        /// <summary>\n        /// Callback when the Preset drop down selection has changed\n        /// </summary>\n        private void DropDownPresetSelect_SelectedIndexChanged(object sender, EventArgs eventArgs)\n        {\n            if (!_isLoad)\n                DropDownPresetSelect_SelectedIndexChanged_IsSave();\n\n            RefreshButtons();\n        }\n\n        /// <summary>\n        /// Callback when the Preset drop down selection has changed during \"save\" mode\n        /// </summary>\n        private void DropDownPresetSelect_SelectedIndexChanged_IsSave()\n        {\n            if (IsCreatePresetSelected)\n            {\n                // show the field to specify a new name when \"create\" option is selected in drop down\n                tbNewPresetName.Enable();\n                lblNewPresetName.Enable();\n            }\n            else\n            {\n                // hide the field to specify a new name when an existing preset is selected\n                tbNewPresetName.Disable();\n                lblNewPresetName.Disable();\n            }\n        }\n\n        /// <summary>\n        /// Refresh the state of the load/save button\n        /// </summary>\n        private void RefreshButtons()\n        {\n            if (_isLoad)\n                btnLoadSave.Enabled = !IsSelectPresetSelected;\n            else\n                btnLoadSave.Enabled = !IsCreatePresetSelected || !IsNewPresetNameFieldEmpty;\n\n            btnDelete.Enabled = !IsCreatePresetSelected && !IsSelectPresetSelected;\n        }\n\n        private bool IsCreatePresetSelected => ddPresetSelect.SelectedItem == ddiCreatePresetItem;\n        private bool IsSelectPresetSelected => ddPresetSelect.SelectedItem == ddiSelectPresetItem;\n        private bool IsNewPresetNameFieldEmpty => string.IsNullOrWhiteSpace(tbNewPresetName.Text);\n\n        /// <summary>\n        /// Populate the preset drop down from saved presets\n        /// </summary>\n        private void LoadPresets()\n        {\n            ddPresetSelect.Items.Clear();\n            ddPresetSelect.Items.Add(_isLoad ? ddiSelectPresetItem : ddiCreatePresetItem);\n            ddPresetSelect.SelectedIndex = 0;\n\n            ddPresetSelect.Items.AddRange(GameOptionPresets.Instance\n                .GetPresetNames()\n                .OrderBy(name => name)\n                .Select(name => new XNADropDownItem()\n                {\n                    Text = name\n                }));\n        }\n\n        /// <summary>\n        /// Show the current window in the \"load\" mode context\n        /// </summary>\n        private void ShowLoad()\n        {\n            LoadPresets();\n\n            // do not show fields to specify a preset name during \"load\" mode\n            lblNewPresetName.Disable();\n            tbNewPresetName.Disable();\n        }\n\n        /// <summary>\n        /// Show the current window in the \"save\" mode context\n        /// </summary>\n        private void ShowSave()\n        {\n            LoadPresets();\n\n            // show fields to specify a preset name during \"save\" mode\n            lblNewPresetName.Enable();\n            tbNewPresetName.Enable();\n            tbNewPresetName.Text = string.Empty;\n        }\n\n        private void BtnLoadSave_LeftClick(object sender, EventArgs e)\n        {\n            var selectedItem = ddPresetSelect.Items[ddPresetSelect.SelectedIndex];\n            if (_isLoad)\n            {\n                PresetLoaded?.Invoke(this, new GameOptionPresetEventArgs(selectedItem.Text));\n            }\n            else\n            {\n                var presetName = IsCreatePresetSelected ? tbNewPresetName.Text : selectedItem.Text;\n                PresetSaved?.Invoke(this, new GameOptionPresetEventArgs(presetName));\n            }\n\n            Disable();\n        }\n\n        private void BtnDelete_LeftClick(object sender, EventArgs e)\n        {\n            var selectedItem = ddPresetSelect.Items[ddPresetSelect.SelectedIndex];\n            var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager,\n                \"Confirm Preset Delete\".L10N(\"Client:Main:ConfirmPresetDeleteTitle\"),\n                \"Are you sure you want to delete this preset?\".L10N(\"Client:Main:ConfirmPresetDeleteText\") + \"\\n\\n\" + selectedItem.Text);\n            messageBox.YesClickedAction = box =>\n            {\n                GameOptionPresets.Instance.DeletePreset(selectedItem.Text);\n                ddPresetSelect.Items.Remove(selectedItem);\n                ddPresetSelect.SelectedIndex = 0;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/MapSharingConfirmationPanel.cs",
    "content": "﻿using ClientGUI;\nusing ClientCore.Extensions;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A panel that is used to verify and display map sharing status.\n    /// </summary>\n    class MapSharingConfirmationPanel : XNAPanel\n    {\n        public MapSharingConfirmationPanel(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        private readonly string MapSharingRequestText = (\"The game host has selected a map that\\ndoesn't exist on your local installation.\").L10N(\"Client:Main:MapSharingRequestText\");\n\n        private readonly string MapSharingDownloadText =\n            \"Downloading map...\".L10N(\"Client:Main:MapSharingDownloadText\");\n\n        private readonly string MapSharingFailedText =\n            (\"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\");\n\n        public event EventHandler MapDownloadConfirmed;\n\n        private XNALabel lblDescription;\n        private XNAClientButton btnDownload;\n\n        public override void Initialize()\n        {\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.TILED;\n\n            Name = nameof(MapSharingConfirmationPanel);\n            BackgroundTexture = AssetLoader.LoadTexture(\"msgboxform.png\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES;\n            lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP;\n            lblDescription.Text = MapSharingRequestText;\n            AddChild(lblDescription);\n\n            Width = lblDescription.Right + UIDesignConstants.EMPTY_SPACE_SIDES;\n\n            btnDownload = new XNAClientButton(WindowManager);\n            btnDownload.Name = nameof(btnDownload);\n            btnDownload.Width = UIDesignConstants.BUTTON_WIDTH_92;\n            btnDownload.Y = lblDescription.Bottom + UIDesignConstants.EMPTY_SPACE_TOP * 2;\n            btnDownload.Text = \"Download\".L10N(\"Client:Main:ButtonDownload\");\n            btnDownload.LeftClick += (s, e) => MapDownloadConfirmed?.Invoke(this, EventArgs.Empty);\n            AddChild(btnDownload);\n            btnDownload.CenterOnParentHorizontally();\n\n            Height = btnDownload.Bottom + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            Disable();\n        }\n\n        public void ShowForMapDownload()\n        {\n            lblDescription.Text = MapSharingRequestText;\n            btnDownload.AllowClick = true;\n            Enable();\n        }\n\n        public void SetDownloadingStatus()\n        {\n            lblDescription.Text = MapSharingDownloadText;\n            btnDownload.AllowClick = false;\n        }\n\n        public void SetFailedStatus()\n        {\n            lblDescription.Text = MapSharingFailedText;\n            btnDownload.AllowClick = false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs",
    "content": "﻿using ClientGUI;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    internal class PasswordRequestWindow : XNAWindow\n    {\n        public PasswordRequestWindow(WindowManager windowManager, PrivateMessagingWindow privateMessagingWindow) : base(windowManager)\n        {\n            this.privateMessagingWindow = privateMessagingWindow;\n        }\n\n        public event EventHandler<PasswordEventArgs> PasswordEntered;\n\n        private XNATextBox tbPassword;\n\n        private HostedCnCNetGame hostedGame;\n\n        private PrivateMessagingWindow privateMessagingWindow;\n        private bool pmWindowWasEnabled { get; set; }\n\n        public override void Initialize()\n        {\n            Name = \"PasswordRequestWindow\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"passwordquerybg.png\");\n\n            var lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = \"lblDescription\";\n            lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            lblDescription.Text = \"Please enter the password for the game and click OK.\".L10N(\"Client:Main:EnterPasswordAndHitOK\");\n\n            ClientRectangle = new Rectangle(0, 0, lblDescription.Width + 24, 110);\n\n            tbPassword = new XNATextBox(WindowManager);\n            tbPassword.Name = \"tbPassword\";\n            tbPassword.ClientRectangle = new Rectangle(lblDescription.X,\n                lblDescription.Bottom + 12, Width - 24, 21);\n\n            var btnOK = new XNAClientButton(WindowManager);\n            btnOK.Name = \"btnOK\";\n            btnOK.ClientRectangle = new Rectangle(lblDescription.X,\n                ClientRectangle.Bottom - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnOK.Text = \"OK\".L10N(\"Client:Main:ButtonOK\");\n            btnOK.LeftClick += BtnOK_LeftClick;\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = \"btnCancel\";\n            btnCancel.ClientRectangle = new Rectangle(Width - 104,\n                btnOK.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(lblDescription);\n            AddChild(tbPassword);\n            AddChild(btnOK);\n            AddChild(btnCancel);\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            EnabledChanged += PasswordRequestWindow_EnabledChanged;\n            tbPassword.EnterPressed += TextBoxPassword_EnterPressed;\n        }\n\n        private void TextBoxPassword_EnterPressed(object sender, EventArgs eventArgs)\n        {\n            BtnOK_LeftClick(this, eventArgs);\n        }\n\n        private void PasswordRequestWindow_EnabledChanged(object sender, EventArgs e)\n        {\n            if (Enabled)\n            {\n                WindowManager.SelectedControl = tbPassword;\n                if (!privateMessagingWindow.Enabled) return;\n                pmWindowWasEnabled = true;\n                privateMessagingWindow.Disable();\n            } \n            else if(pmWindowWasEnabled)\n            {\n                privateMessagingWindow.Enable();\n            }\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        private void BtnOK_LeftClick(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbPassword.Text))\n                return;\n\n            pmWindowWasEnabled = false;\n            Disable();\n\n            PasswordEntered?.Invoke(this, new PasswordEventArgs(tbPassword.Text, hostedGame));\n            tbPassword.Text = string.Empty;\n        }\n\n        public void SetHostedGame(HostedCnCNetGame hostedGame)\n        {\n            this.hostedGame = hostedGame;\n        }\n    }\n\n    public class PasswordEventArgs : EventArgs\n    {\n        public PasswordEventArgs(string password, HostedCnCNetGame hostedGame)\n        {\n            Password = password;\n            HostedGame = hostedGame;\n        }\n\n        /// <summary>\n        /// The password input by the user.\n        /// </summary>\n        public string Password { get; private set; }\n\n        /// <summary>\n        /// The game that the user is attempting to join.\n        /// </summary>\n        public HostedCnCNetGame HostedGame { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessageNotificationBox.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.IO;\nusing System.Reflection;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing SixLabors.ImageSharp;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A box that notifies users of new private messages,\n    /// top-right of the game window.\n    /// </summary>\n    public class PrivateMessageNotificationBox : XNAPanel\n    {\n        const double DOWN_TIME_WAIT_SECONDS = 4.0;\n        const double DOWN_MOVEMENT_RATE = 2.0;\n        const double UP_MOVEMENT_RATE = 2.0;\n\n        public PrivateMessageNotificationBox(WindowManager windowManager) : base(windowManager)\n        {\n            downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS);\n        }\n\n        XNALabel lblSender;\n        XNAPanel gameIconPanel;\n        XNALabel lblMessage;\n\n        TimeSpan downTime = TimeSpan.Zero;\n\n        TimeSpan downTimeWaitTime;\n\n        bool isDown = false;\n\n        double locationY = -100.0;\n\n        public override void Initialize()\n        {\n            Name = \"PrivateMessageNotificationBox\";\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 196), 1, 1);\n            ClientRectangle = new Rectangle(WindowManager.RenderResolutionX - 300, -100, 300, 100);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            XNALabel lblHeader = new XNALabel(WindowManager);\n            lblHeader.Name = \"lblHeader\";\n            lblHeader.FontIndex = 1;\n            lblHeader.Text = \"PRIVATE MESSAGE\".L10N(\"Client:Main:PMHeader\");\n            AddChild(lblHeader);\n            lblHeader.CenterOnParent();\n            lblHeader.ClientRectangle = new Rectangle(lblHeader.X,\n                6, lblHeader.Width, lblHeader.Height);\n\n            XNAPanel linePanel = new XNAPanel(WindowManager);\n            linePanel.Name = \"linePanel\";\n            linePanel.ClientRectangle = new Rectangle(0, Height - 20, Width, 1);\n\n            XNALabel lblHint = new XNALabel(WindowManager);\n            lblHint.Name = \"lblHint\";\n            lblHint.RemapColor = UISettings.ActiveSettings.SubtleTextColor;\n            lblHint.Text = \"Press F4 to respond\".L10N(\"Client:Main:F4ToRespond\");\n\n            AddChild(lblHint);\n            lblHint.CenterOnParent();\n            lblHint.ClientRectangle = new Rectangle(lblHint.X,\n                linePanel.Y + 3,\n                lblHint.Width, lblHint.Height);\n\n            gameIconPanel = new XNAPanel(WindowManager);\n            gameIconPanel.Name = \"gameIconPanel\";\n            gameIconPanel.ClientRectangle = new Rectangle(12, 30, 16, 16);\n            gameIconPanel.DrawBorders = false;\n\n            var assembly = Assembly.GetAssembly(typeof(GameCollection));\n            using Stream dtaIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.dtaicon.png\");\n            using var dtaIcon = Image.Load(dtaIconStream);\n\n            gameIconPanel.BackgroundTexture = AssetLoader.TextureFromImage(dtaIcon);\n\n            lblSender = new XNALabel(WindowManager);\n            lblSender.Name = \"lblSender\";\n            lblSender.FontIndex = 1;\n            lblSender.ClientRectangle = new Rectangle(gameIconPanel.Right + 3,\n                gameIconPanel.Y, 0, 0);\n            lblSender.Text = \"Rampastring:\";\n\n            lblMessage = new XNALabel(WindowManager);\n            lblMessage.Name = \"lblMessage\";\n            lblMessage.ClientRectangle = new Rectangle(12, lblSender.Bottom + 6, 0, 0);\n            lblMessage.RemapColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ReceivedPMColor);\n            lblMessage.Text = \"This is a test message.\";\n\n            AddChild(gameIconPanel);\n            AddChild(linePanel);\n            AddChild(lblSender);\n            AddChild(lblMessage);\n\n            base.Initialize();\n        }\n\n        public void Show(Texture2D gameIcon, string sender, string message)\n        {\n            Visible = true;\n            Enabled = true;\n            gameIconPanel.BackgroundTexture = gameIcon;\n            lblSender.Text = sender + \":\";\n            lblMessage.Text = message;\n\n            if (lblMessage.Right > Width)\n            {\n                while (lblMessage.Right > Width)\n                {\n                    lblMessage.Text = lblMessage.Text.Remove(lblMessage.Text.Length - 1);\n                }\n\n                if (lblMessage.Text.Length > 3)\n                {\n                    lblMessage.Text = lblMessage.Text.Remove(lblMessage.Text.Length - 3) + \"...\";\n                }\n            }\n\n            downTime = TimeSpan.Zero;\n            isDown = true;\n        }\n\n        public void Hide()\n        {\n            isDown = false;\n            locationY = -Height;\n            ClientRectangle = new Rectangle(X, (int)locationY,\n                Width, Height);\n            Visible = false;\n            Enabled = false;\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (isDown)\n            {\n                if (locationY < 0)\n                {\n                    locationY += DOWN_MOVEMENT_RATE;\n                    ClientRectangle = new Rectangle(X, (int)locationY,\n                        Width, Height);\n                }\n\n                if (WindowManager.HasFocus)\n                {\n                    downTime += gameTime.ElapsedGameTime;\n                    isDown = downTime < downTimeWaitTime;\n                }\n            }\n            else\n            {\n                if (locationY > -Height)\n                {\n                    locationY -= UP_MOVEMENT_RATE;\n                    ClientRectangle = new Rectangle(X, (int)locationY, Width, Height);\n                }\n                else\n                {\n                    Visible = false;\n                    Enabled = false;\n                }\n            }\n\n            base.Update(gameTime);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingPanel.cs",
    "content": "﻿using ClientGUI;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A panel that hides itself if it's clicked while none of its children\n    /// are the focus of input.\n    /// </summary>\n    public class PrivateMessagingPanel : DarkeningPanel\n    {\n        public PrivateMessagingPanel(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public override void OnLeftClick(InputEventArgs inputEventArgs)\n        {\n            inputEventArgs.Handled = true;\n            \n            if (GetActiveChild() == null)\n                Hide();\n\n            base.OnLeftClick(inputEventArgs);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientGUI;\nusing DTAClient.Online;\nusing DTAClient.Online.EventArguments;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\nusing SixLabors.ImageSharp;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class PrivateMessagingWindow : XNAWindow, ISwitchable\n    {\n        private const int MESSAGES_INDEX = 0;\n        private const int FRIEND_LIST_VIEW_INDEX = 1;\n        private const int ALL_PLAYERS_VIEW_INDEX = 2;\n        private const int RECENT_PLAYERS_VIEW_INDEX = 3;\n\n        private const int LB_USERS_WIDTH = 150;\n\n        private readonly string DEFAULT_PLAYERS_TEXT = \"PLAYERS:\".L10N(\"Client:Main:Players\");\n        private readonly string RECENT_PLAYERS_TEXT = \"RECENT PLAYERS:\".L10N(\"Client:Main:RecentPlayers\");\n\n        private CnCNetUserData cncnetUserData;\n        private readonly PrivateMessageHandler privateMessageHandler;\n\n        public PrivateMessagingWindow(\n            WindowManager windowManager,\n            CnCNetManager connectionManager,\n            GameCollection gameCollection,\n            CnCNetUserData cncnetUserData,\n            PrivateMessageHandler privateMessageHandler\n        ) : base(windowManager)\n        {\n            this.gameCollection = gameCollection;\n            this.connectionManager = connectionManager;\n            this.cncnetUserData = cncnetUserData;\n            this.privateMessageHandler = privateMessageHandler;\n        }\n\n        private XNALabel lblPrivateMessaging;\n\n        private XNAClientTabControl tabControl;\n\n        private XNALabel lblPlayers;\n        private XNAListBox lbUserList;\n        private RecentPlayerTable mclbRecentPlayerList;\n\n        private XNALabel lblMessages;\n        private ChatListBox lbMessages;\n\n        private XNATextBox tbMessageInput;\n\n        private GlobalContextMenu globalContextMenu;\n\n        private CnCNetManager connectionManager;\n\n        private GameCollection gameCollection;\n\n        private Texture2D unknownGameIcon;\n        private Texture2D adminGameIcon;\n\n        private Color personalMessageColor;\n        private Color otherUserMessageColor;\n\n        private string lastReceivedPMSender;\n        private string lastConversationPartner;\n\n        /// <summary>\n        /// Holds the users that the local user has had conversations with\n        /// during this client session.\n        /// </summary>\n        private List<PrivateMessageUser> privateMessageUsers = new List<PrivateMessageUser>();\n\n        private PrivateMessageNotificationBox notificationBox;\n\n        private EnhancedSoundEffect sndPrivateMessageSound;\n        private EnhancedSoundEffect sndMessageSound;\n\n        /// <summary>\n        /// Because the user cannot view PMs during a game, we store the latest\n        /// PM received during a game in this variable and display it when the\n        /// user has returned from the game.\n        /// </summary>\n        private PrivateMessage pmReceivedDuringGame;\n\n        // These are used by the \"invite to game\" feature in the\n        // context menu and are kept up-to-date by the lobby\n        private string inviteChannelName;\n        private string inviteGameName;\n        private string inviteChannelPassword;\n\n        private Action<IRCUser, IMessageView> JoinUserAction;\n\n        public override void Initialize()\n        {\n            Name = nameof(PrivateMessagingWindow);\n            ClientRectangle = new Rectangle(0, 0, 600, 600);\n            BackgroundTexture = AssetLoader.LoadTextureUncached(\"privatemessagebg.png\");\n\n            var assembly = Assembly.GetAssembly(typeof(GameCollection));\n            using Stream unknownIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.unknownicon.png\");\n            using Stream cncnetIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.cncneticon.png\");\n\n            unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream));\n            adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream));\n\n            personalMessageColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.SentPMColor);\n            otherUserMessageColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ReceivedPMColor);\n\n            lblPrivateMessaging = new XNALabel(WindowManager);\n            lblPrivateMessaging.Name = nameof(lblPrivateMessaging);\n            lblPrivateMessaging.FontIndex = 1;\n            lblPrivateMessaging.Text = \"PRIVATE MESSAGING\".L10N(\"Client:Main:PMLabel\");\n\n            AddChild(lblPrivateMessaging);\n            lblPrivateMessaging.CenterOnParent();\n            lblPrivateMessaging.ClientRectangle = new Rectangle(\n                lblPrivateMessaging.X, 12,\n                lblPrivateMessaging.Width,\n                lblPrivateMessaging.Height);\n\n            tabControl = new XNAClientTabControl(WindowManager);\n            tabControl.Name = nameof(tabControl);\n            tabControl.ClientRectangle = new Rectangle(34, 50, 0, 0);\n            tabControl.ClickSound = new EnhancedSoundEffect(\"button.wav\");\n            tabControl.FontIndex = 1;\n            tabControl.AddTab(\"Messages\".L10N(\"Client:Main:MessagesTab\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.AddTab(\"Friend List\".L10N(\"Client:Main:FriendListTab\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.AddTab(\"All Players\".L10N(\"Client:Main:AllPlayersTab\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.AddTab(\"Recent Players\".L10N(\"Client:Main:RecentPlayersTab\"), UIDesignConstants.BUTTON_WIDTH_133);\n            tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged;\n\n            lblPlayers = new XNALabel(WindowManager);\n            lblPlayers.Name = nameof(lblPlayers);\n            lblPlayers.ClientRectangle = new Rectangle(12, tabControl.Bottom + 24, 0, 0);\n            lblPlayers.FontIndex = 1;\n            lblPlayers.Text = DEFAULT_PLAYERS_TEXT;\n\n            lbUserList = new XNAListBox(WindowManager);\n            lbUserList.Name = nameof(lbUserList);\n            lbUserList.ClientRectangle = new Rectangle(lblPlayers.X,\n                lblPlayers.Bottom + 6,\n                LB_USERS_WIDTH, Height - lblPlayers.Bottom - 18);\n            lbUserList.RightClick += LbUserList_RightClick;\n            lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged;\n            lbUserList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbUserList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbUserList.DoubleLeftClick += UserList_LeftDoubleClick;\n\n            lblMessages = new XNALabel(WindowManager);\n            lblMessages.Name = nameof(lblMessages);\n            lblMessages.ClientRectangle = new Rectangle(lbUserList.Right + 12,\n                lblPlayers.Y, 0, 0);\n            lblMessages.FontIndex = 1;\n            lblMessages.Text = \"MESSAGES:\".L10N(\"Client:Main:Messages\");\n\n            lbMessages = new ChatListBox(WindowManager);\n            lbMessages.Name = nameof(lbMessages);\n            lbMessages.ClientRectangle = new Rectangle(lblMessages.X,\n                lbUserList.Y,\n                Width - lblMessages.X - 12,\n                lbUserList.Height - 25);\n            lbMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbMessages.RightClick += ChatListBox_RightClick;\n\n            tbMessageInput = new XNATextBox(WindowManager);\n            tbMessageInput.Name = nameof(tbMessageInput);\n            tbMessageInput.ClientRectangle = new Rectangle(lbMessages.X,\n                lbMessages.Bottom + 6, lbMessages.Width, 19);\n            tbMessageInput.EnterPressed += TbMessageInput_EnterPressed;\n            tbMessageInput.MaximumTextLength = 200;\n            tbMessageInput.Enabled = false;\n\n            mclbRecentPlayerList = new RecentPlayerTable(WindowManager, connectionManager);\n            mclbRecentPlayerList.ClientRectangle = new Rectangle(lbUserList.X, lbUserList.Y, lbMessages.Right - lbUserList.X, lbUserList.Height);\n            mclbRecentPlayerList.PlayerRightClick += RecentPlayersList_RightClick;\n            mclbRecentPlayerList.Disable();\n\n            globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, this);\n            globalContextMenu.JoinEvent += PlayerContextMenu_JoinUser;\n\n            notificationBox = new PrivateMessageNotificationBox(WindowManager);\n            notificationBox.Enabled = false;\n            notificationBox.Visible = false;\n            notificationBox.LeftClick += NotificationBox_LeftClick;\n\n            AddChild(tabControl);\n            AddChild(lblPlayers);\n            AddChild(lbUserList);\n            AddChild(lblMessages);\n            AddChild(lbMessages);\n            AddChild(tbMessageInput);\n            AddChild(mclbRecentPlayerList);\n            AddChild(globalContextMenu);\n            WindowManager.AddAndInitializeControl(notificationBox);\n\n            base.Initialize();\n\n            CenterOnParent();\n\n            tabControl.SelectedTab = MESSAGES_INDEX;\n\n            privateMessageHandler.PrivateMessageReceived += PrivateMessageHandler_PrivateMessageReceived;\n            connectionManager.UserAdded += ConnectionManager_UserAdded;\n            connectionManager.UserRemoved += ConnectionManager_UserRemoved;\n            connectionManager.UserGameIndexUpdated += ConnectionManager_UserGameIndexUpdated;\n\n            sndMessageSound = new EnhancedSoundEffect(\"message.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown);\n\n            sndPrivateMessageSound = new EnhancedSoundEffect(\"pm.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundPrivateMessageCooldown);\n\n            sndMessageSound.Enabled = UserINISettings.Instance.MessageSound;\n\n            GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited;\n        }\n\n        private void ChatListBox_RightClick(object sender, EventArgs e)\n        {\n            if (lbMessages.HoveredIndex < 0 || lbMessages.HoveredIndex >= lbMessages.Items.Count)\n                return;\n\n            lbMessages.SelectedIndex = lbMessages.HoveredIndex;\n            var chatMessage = lbMessages.SelectedItem.Tag as ChatMessage;\n            if (chatMessage == null)\n                return;\n\n            globalContextMenu.Show(chatMessage, GetCursorPoint());\n        }\n\n        private void UserList_LeftDoubleClick(object sender, EventArgs e)\n        {\n            if (lbUserList.SelectedItem != null)\n                tabControl.SelectedTab = MESSAGES_INDEX;\n        }\n\n        private void RecentPlayersList_RightClick(object sender, RecentPlayerTableRightClickEventArgs e)\n            => globalContextMenu.Show(e.IrcUser, GetCursorPoint());\n\n        private void ConnectionManager_UserGameIndexUpdated(object sender, UserEventArgs e)\n        {\n            var userItem = FindItemForName(e.User.Name);\n\n            if (userItem != null)\n                userItem.Texture = GetUserTexture(e.User);\n        }\n\n        private void ConnectionManager_UserRemoved(object sender, UserNameIndexEventArgs e)\n        {\n            var pmUser = privateMessageUsers.Find(pmsgUser => pmsgUser.IrcUser.Name == e.UserName);\n            ChatMessage leaveMessage = null;\n\n            if (pmUser != null)\n            {\n                leaveMessage = new ChatMessage(Color.White,\n                    string.Format(\"{0} is now offline.\".L10N(\"Client:Main:PlayerOffline\"), e.UserName));\n                pmUser.Messages.Add(leaveMessage);\n            }\n\n            if (tabControl.SelectedTab == ALL_PLAYERS_VIEW_INDEX)\n            {\n                if (e.UserIndex >= lbUserList.Items.Count || e.UserIndex < 0)\n                    return;\n\n                if (e.UserIndex == lbUserList.SelectedIndex)\n                {\n                    lbUserList.SelectedIndex = -1;\n                }\n                else if (e.UserIndex < lbUserList.SelectedIndex)\n                {\n                    lbUserList.SelectedIndexChanged -= LbUserList_SelectedIndexChanged;\n                    lbUserList.SelectedIndex--;\n                    lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged;\n                }\n\n                lbUserList.Items.RemoveAt(e.UserIndex);\n            }\n            else\n            {\n                XNAListBoxItem lbItem = FindItemForName(e.UserName);\n\n                if (lbItem != null)\n                {\n                    lbItem.TextColor = UISettings.ActiveSettings.DisabledItemColor;\n                    lbItem.Texture = null;\n                    lbItem.Tag = null;\n\n                    if (lbItem == lbUserList.SelectedItem && leaveMessage != null)\n                    {\n                        tbMessageInput.Enabled = false;\n                        lbMessages.AddMessage(leaveMessage);\n                    }\n                }\n            }\n        }\n\n        private void ConnectionManager_UserAdded(object sender, UserEventArgs e)\n        {\n            var pmUser = privateMessageUsers.Find(pmsgUser => pmsgUser.IrcUser.Name == e.User.Name);\n\n            ChatMessage joinMessage = null;\n\n            if (pmUser != null)\n            {\n                joinMessage = new ChatMessage(string.Format(\"{0} is now online.\".L10N(\"Client:Main:PlayerOnline\"), e.User.Name));\n                pmUser.Messages.Add(joinMessage);\n            }\n\n            if (tabControl.SelectedTab == ALL_PLAYERS_VIEW_INDEX)\n            {\n                RefreshAllUsers();\n            }\n            else // if (tabControl.SelectedTab == 0 or 1)\n            {\n                XNAListBoxItem lbItem = FindItemForName(e.User.Name);\n\n                if (lbItem != null)\n                {\n                    lbItem.Tag = e.User;\n                    lbItem.Texture = GetUserTexture(e.User);\n\n                    if (lbItem == lbUserList.SelectedItem)\n                    {\n                        tbMessageInput.Enabled = true;\n\n                        if (joinMessage != null)\n                            lbMessages.AddMessage(joinMessage);\n                    }\n                }\n            }\n        }\n\n        private void RefreshAllUsers()\n        {\n            lbUserList.SelectedIndexChanged -= LbUserList_SelectedIndexChanged;\n\n            string selectedUserName = string.Empty;\n\n            var selectedItem = lbUserList.SelectedItem;\n            if (selectedItem != null)\n                selectedUserName = selectedItem.Text;\n\n            lbUserList.Clear();\n\n            foreach (var ircUser in connectionManager.UserList)\n            {\n                var item = new XNAListBoxItem(ircUser.Name);\n                item.Tag = ircUser;\n                item.Texture = GetUserTexture(ircUser);\n                lbUserList.AddItem(item);\n            }\n\n            lbUserList.SelectedIndex = FindItemIndexForName(selectedUserName);\n\n            if (lbUserList.SelectedIndex == -1)\n            {\n                // If we previously had an user selected and they now went offline,\n                // clear the messages and message input\n                tbMessageInput.Text = string.Empty;\n                tbMessageInput.Enabled = false;\n                lbMessages.Clear();\n                lbMessages.SelectedIndex = -1;\n                lbMessages.TopIndex = 0;\n            }\n\n            lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged;\n        }\n\n        public void SetInviteChannelInfo(string channelName, string gameName, string channelPassword)\n        {\n            inviteChannelName = channelName;\n            inviteGameName = gameName;\n            inviteChannelPassword = channelPassword;\n        }\n\n        public void ClearInviteChannelInfo() => SetInviteChannelInfo(string.Empty, string.Empty, string.Empty);\n\n        private void NotificationBox_LeftClick(object sender, EventArgs e) => SwitchOn();\n\n        private void LbUserList_RightClick(object sender, EventArgs e)\n        {\n            lbUserList.SelectedIndex = lbUserList.HoveredIndex;\n            var ircUser = (IRCUser)lbUserList.SelectedItem?.Tag;\n            if (ircUser == null)\n                return;\n\n            globalContextMenu.Show(new GlobalContextMenuData()\n            {\n                IrcUser = ircUser,\n                inviteChannelName = inviteChannelName,\n                inviteChannelPassword = inviteChannelPassword,\n                inviteGameName = inviteGameName\n            }, GetCursorPoint());\n        }\n\n        private void PlayerContextMenu_JoinUser(object sender, JoinUserEventArgs args)\n        {\n            if (tabControl.SelectedTab == RECENT_PLAYERS_VIEW_INDEX)\n                JoinUserAction(args.IrcUser, new RecentPlayerMessageView(WindowManager));\n            else\n                JoinUserAction(args.IrcUser, lbMessages);\n        }\n\n        private void SharedUILogic_GameProcessExited() =>\n            WindowManager.AddCallback(new Action(HandleGameProcessExited), null);\n\n        private void HandleGameProcessExited()\n        {\n            if (pmReceivedDuringGame != null)\n            {\n                ShowNotification(pmReceivedDuringGame.User, pmReceivedDuringGame.Message);\n                pmReceivedDuringGame = null;\n            }\n        }\n\n        private bool IsPlayerOnline(string playerName) => !string.IsNullOrEmpty(playerName) && connectionManager.UserList.Find(u => u.Name == playerName) != null;\n\n        private void PrivateMessageHandler_PrivateMessageReceived(object sender, PrivateMessageEventArgs e)\n        {\n            if (UserINISettings.Instance.AllowPrivateMessagesFromState == (int)AllowPrivateMessagesFromEnum.None)\n                return;\n\n            PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == e.Sender);\n\n            if (pmUser == null)\n            {\n                pmUser = new PrivateMessageUser(e.ircUser);\n                privateMessageUsers.Add(pmUser);\n\n                if (tabControl.SelectedTab == MESSAGES_INDEX)\n                {\n                    string selecterUserName = string.Empty;\n\n                    if (lbUserList.SelectedItem != null)\n                        selecterUserName = lbUserList.SelectedItem.Text;\n\n                    lbUserList.Clear();\n                    privateMessageUsers.ForEach(pmsgUser => AddPlayerToList(pmsgUser.IrcUser,\n                        IsPlayerOnline(pmsgUser.IrcUser.Name)));\n\n                    lbUserList.SelectedIndex = FindItemIndexForName(selecterUserName);\n                }\n            }\n\n            bool isFriend = cncnetUserData.IsFriend(pmUser.IrcUser.Name);\n            if (UserINISettings.Instance.AllowPrivateMessagesFromState == (int)AllowPrivateMessagesFromEnum.Friends && !isFriend)\n                return;\n\n            // Exclude messages from users not in the current channel\n            if (!isFriend &&\n                UserINISettings.Instance.AllowPrivateMessagesFromState != (int)AllowPrivateMessagesFromEnum.All &&\n                connectionManager.MainChannel.Users.Find(e.Sender) == null)\n                return;\n\n            ChatMessage message = new ChatMessage(e.Sender, otherUserMessageColor, DateTime.Now, e.Message);\n\n            pmUser.Messages.Add(message);\n\n            lastReceivedPMSender = e.Sender;\n            lastConversationPartner = e.Sender;\n\n            if (!Visible)\n            {\n                HandleNotification(pmUser.IrcUser, e.Message);\n\n                if (lbUserList.SelectedItem == null || lbUserList.SelectedItem.Text != e.Sender)\n                    return;\n            }\n            else if (lbUserList.SelectedItem == null || lbUserList.SelectedItem.Text != e.Sender)\n            {\n                HandleNotification(pmUser.IrcUser, e.Message);\n                return;\n            }\n\n            lbMessages.AddMessage(message);\n            if (sndMessageSound != null)\n                sndMessageSound.Play();\n        }\n\n        /// <summary>\n        /// Displays a PM message if the user is not in-game, and queues\n        /// it to be displayed after the game if the user is in-game.\n        /// </summary>\n        /// <param name=\"ircUser\">The sender of the private message.</param>\n        /// <param name=\"message\">The contents of the private message.</param>\n        private void HandleNotification(IRCUser ircUser, string message)\n        {\n            if (!ProgramConstants.IsInGame)\n            {\n                ShowNotification(ircUser, message);\n            }\n            else\n                pmReceivedDuringGame = new PrivateMessage(ircUser, message);\n        }\n\n        private void ShowNotification(IRCUser ircUser, string message)\n        {\n            if (!UserINISettings.Instance.DisablePrivateMessagePopups)\n                notificationBox.Show(GetUserTexture(ircUser), ircUser.Name, message);\n            else\n                privateMessageHandler.IncrementUnreadMessageCount();\n\n            if (sndPrivateMessageSound != null)\n                sndPrivateMessageSound.Play();\n        }\n\n        private Predicate<XNAListBoxItem> MatchItemForName(string userName) => item => ((IRCUser)item.Tag)?.Name == userName;\n\n        private XNAListBoxItem FindItemForName(string userName) => lbUserList.Items.Find(MatchItemForName(userName));\n\n        private int FindItemIndexForName(string userName) => lbUserList.Items.FindIndex(MatchItemForName(userName));\n\n        private void TbMessageInput_EnterPressed(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbMessageInput.Text))\n                return;\n\n            if (lbUserList.SelectedItem == null)\n                return;\n\n            string userName = lbUserList.SelectedItem.Text;\n\n            connectionManager.SendCustomMessage(new QueuedMessage(\"PRIVMSG \" + userName + \" :\" + tbMessageInput.Text,\n                QueuedMessageType.CHAT_MESSAGE, 0));\n\n            PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName);\n            if (pmUser == null)\n            {\n                IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName);\n\n                if (iu == null)\n                {\n                    Logger.Log(\"Null IRCUser in private messaging?\");\n                    return;\n                }\n\n                pmUser = new PrivateMessageUser(iu);\n                privateMessageUsers.Add(pmUser);\n            }\n\n            ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME,\n                personalMessageColor, DateTime.Now, tbMessageInput.Text);\n\n            pmUser.Messages.Add(sentMessage);\n\n            lbMessages.AddMessage(sentMessage);\n            if (sndMessageSound != null)\n                sndMessageSound.Play();\n\n            lastConversationPartner = userName;\n\n            if (tabControl.SelectedTab != MESSAGES_INDEX)\n            {\n                tabControl.SelectedTab = MESSAGES_INDEX;\n                lbUserList.SelectedIndex = FindItemIndexForName(userName);\n            }\n\n            tbMessageInput.Text = string.Empty;\n        }\n\n        private void LbUserList_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            lbMessages.Clear();\n            lbMessages.SelectedIndex = -1;\n            lbMessages.TopIndex = 0;\n            tbMessageInput.Text = string.Empty;\n\n            if (lbUserList.SelectedItem == null)\n            {\n                tbMessageInput.Enabled = false;\n                return;\n            }\n\n            var ircUser = (IRCUser)lbUserList.SelectedItem.Tag;\n            tbMessageInput.Enabled = IsPlayerOnline(ircUser?.Name);\n\n            var pmUser = privateMessageUsers.Find(u =>\n                u.IrcUser.Name == lbUserList.SelectedItem.Text);\n\n            if (pmUser == null)\n            {\n                return;\n            }\n\n            foreach (ChatMessage message in pmUser.Messages)\n            {\n                lbMessages.AddMessage(message);\n            }\n\n            lbMessages.ScrollToBottom();\n        }\n\n        private void MessagesTabSelected()\n        {\n            ShowRecentPlayers(false);\n            var _privateMessageUsers = privateMessageUsers.Select(pMsgUser =>\n                new\n                {\n                    ircUser = pMsgUser.IrcUser,\n                    isFriend = cncnetUserData.FriendList.Contains(pMsgUser.IrcUser.Name),\n                    isOnline = connectionManager.UserList.Any(u => u.Name == pMsgUser.IrcUser.Name)\n                });\n\n            var sortedPrivateMessageUsers = _privateMessageUsers\n                .OrderBy(pMsgUser => !pMsgUser.isOnline)\n                .ThenBy(pMsgUser => !pMsgUser.isFriend)\n                .ThenBy(pMsguser => pMsguser.ircUser.Name);\n\n            foreach (var pMsgUser in sortedPrivateMessageUsers)\n                AddPlayerToList(pMsgUser.ircUser, pMsgUser.isOnline);\n        }\n\n        private void FriendsListTabSelected()\n        {\n            ShowRecentPlayers(false);\n            var friends = cncnetUserData.FriendList.Select(friendName =>\n            {\n                var ircUser = connectionManager.UserList.Find(u => u.Name == friendName);\n\n                return new\n                {\n                    ircUser = ircUser ?? new IRCUser(friendName),\n                    isOnline = ircUser != null\n                };\n            });\n\n            friends\n                .OrderBy(friend => !friend.isOnline)\n                .ThenBy(friend => friend.ircUser.Name)\n                .ToList()\n                .ForEach(friend => AddPlayerToList(friend.ircUser, friend.isOnline));\n        }\n\n        private void RecentPlayersTabSelected()\n        {\n            ShowRecentPlayers(true);\n            var recentPlayers = cncnetUserData.RecentList.OrderByDescending(rp => rp.GameTime);\n            mclbRecentPlayerList.ClearItems();\n\n            foreach (RecentPlayer recentPlayer in recentPlayers)\n                mclbRecentPlayerList.AddRecentPlayer(recentPlayer);\n        }\n\n        private void AllPlayersTabSelected()\n        {\n            ShowRecentPlayers(false);\n\n            foreach (var user in connectionManager.UserList)\n                AddPlayerToList(user, true);\n        }\n\n        private void ShowRecentPlayers(bool show)\n        {\n            if (show)\n            {\n                lbMessages.Disable();\n                tbMessageInput.Disable();\n                lblMessages.Disable();\n                lbUserList.Disable();\n                lblPlayers.Text = RECENT_PLAYERS_TEXT;\n                mclbRecentPlayerList.Enable();\n            }\n            else\n            {\n                lbMessages.Enable();\n                tbMessageInput.Enable();\n                lblMessages.Enable();\n                lbUserList.Enable();\n                lblPlayers.Text = DEFAULT_PLAYERS_TEXT;\n                mclbRecentPlayerList.Disable();\n            }\n        }\n\n        private void TabControl_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            lbMessages.Clear();\n            lbMessages.SelectedIndex = -1;\n            lbMessages.TopIndex = 0;\n            lbUserList.Clear();\n            lbUserList.SelectedIndex = -1;\n            lbUserList.TopIndex = 0;\n            tbMessageInput.Text = string.Empty;\n\n            switch (tabControl.SelectedTab)\n            {\n                case MESSAGES_INDEX:\n                    MessagesTabSelected();\n                    break;\n                case FRIEND_LIST_VIEW_INDEX:\n                    FriendsListTabSelected();\n                    break;\n                case ALL_PLAYERS_VIEW_INDEX:\n                    AllPlayersTabSelected();\n                    break;\n                case RECENT_PLAYERS_VIEW_INDEX:\n                    RecentPlayersTabSelected();\n                    break;\n            }\n        }\n\n        private void AddPlayerToList(IRCUser user, bool isOnline, string label = null)\n        {\n            XNAListBoxItem lbItem = new XNAListBoxItem();\n            lbItem.Text = label ?? user.Name;\n\n            lbItem.TextColor = isOnline ?\n                UISettings.ActiveSettings.AltColor : UISettings.ActiveSettings.DisabledItemColor;\n            lbItem.Tag = user;\n            lbItem.Texture = isOnline ? GetUserTexture(user) : null;\n\n            lbUserList.AddItem(lbItem);\n        }\n\n        private Texture2D GetUserTexture(IRCUser user)\n        {\n            if (user.GameID < 0 || user.GameID >= gameCollection.GameList.Count)\n                return unknownGameIcon;\n            else\n                return gameCollection.GameList[user.GameID].Texture;\n        }\n\n        /// <summary>\n        /// Prepares a recipient for sending a private message.\n        /// </summary>\n        /// <param name=\"name\"></param>\n        public void InitPM(string name)\n        {\n            Visible = true;\n            Enabled = true;\n\n            // Check if we've already talked with the user during this session\n            // and if so, open the old conversation\n            int pmUserIndex = privateMessageUsers.FindIndex(\n                pmUser => pmUser.IrcUser.Name == name);\n\n            if (pmUserIndex > -1)\n            {\n                tabControl.SelectedTab = MESSAGES_INDEX;\n                lbUserList.SelectedIndex = FindItemIndexForName(name);\n                WindowManager.SelectedControl = tbMessageInput;\n                return;\n            }\n\n            if (cncnetUserData.IsFriend(name))\n            {\n                // If we haven't talked with the user, check if they are a friend and if so,\n                // let's enter the friend list and talk to them there\n                tabControl.SelectedTab = FRIEND_LIST_VIEW_INDEX;\n            }\n            else\n            {\n                // If the user isn't a friend, switch to the \"all players\" view and\n                // open the conversation there\n                tabControl.SelectedTab = ALL_PLAYERS_VIEW_INDEX;\n            }\n\n            lbUserList.SelectedIndex = FindItemIndexForName(name);\n\n            if (lbUserList.SelectedIndex > -1)\n            {\n                WindowManager.SelectedControl = tbMessageInput;\n\n                lbUserList.TopIndex = lbUserList.SelectedIndex > -1 ? lbUserList.SelectedIndex : 0;\n            }\n\n            if (lbUserList.LastIndex - lbUserList.TopIndex < lbUserList.NumberOfLinesOnList - 1)\n                lbUserList.ScrollToBottom();\n        }\n\n        public void SwitchOn()\n        {\n            tabControl.SelectedTab = MESSAGES_INDEX;\n            notificationBox.Hide();\n\n            WindowManager.SelectedControl = null;\n            privateMessageHandler.ResetUnreadMessageCount();\n\n            if (Visible)\n            {\n                if (!string.IsNullOrEmpty(lastReceivedPMSender))\n                {\n                    int index = FindItemIndexForName(lastReceivedPMSender);\n\n                    if (index > -1)\n                        lbUserList.SelectedIndex = index;\n                }\n            }\n            else\n            {\n                Enable();\n\n                if (!string.IsNullOrEmpty(lastConversationPartner))\n                {\n                    int index = FindItemIndexForName(lastConversationPartner);\n\n                    if (index > -1)\n                    {\n                        lbUserList.SelectedIndex = index;\n                        WindowManager.SelectedControl = tbMessageInput;\n                    }\n                }\n            }\n        }\n\n        public void SetJoinUserAction(Action<IRCUser, IMessageView> joinUserAction)\n        {\n            JoinUserAction = joinUserAction;\n        }\n\n        public void SwitchOff() => Disable();\n\n        public string GetSwitchName() => \"Private Messaging\".L10N(\"Client:Main:PrivateMessaging\");\n\n        /// <summary>\n        /// A class for storing a private message in memory.\n        /// </summary>\n        class PrivateMessage\n        {\n            public PrivateMessage(IRCUser user, string message)\n            {\n                User = user;\n                Message = message;\n            }\n\n            public IRCUser User;\n            public string Message;\n        }\n\n        class RecentPlayerMessageView : IMessageView\n        {\n            private readonly WindowManager windowManager;\n\n            public RecentPlayerMessageView(WindowManager windowManager)\n            {\n                this.windowManager = windowManager;\n            }\n\n            public void AddMessage(ChatMessage message)\n                => XNAMessageBox.Show(windowManager, \"Message\".L10N(\"Client:Main:MessageTitle\"), message.Message);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing DTAClient.Online;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class RecentPlayerTable : XNAMultiColumnListBox\n    {\n        private readonly CnCNetManager connectionManager;\n\n        public EventHandler<RecentPlayerTableRightClickEventArgs> PlayerRightClick;\n\n        public RecentPlayerTable(WindowManager windowManager, CnCNetManager connectionManager) : base(windowManager)\n        {\n            this.connectionManager = connectionManager;\n        }\n\n        public override void Initialize()\n        {\n            AllowRightClickUnselect = false;\n            \n            base.Initialize();\n\n            AddColumn(\"Player\".L10N(\"Client:Main:RecentPlayerPlayer\"));\n            AddColumn(\"Game\".L10N(\"Client:Main:RecentPlayerGame\"));\n            AddColumn(\"Date/Time\".L10N(\"Client:Main:RecentPlayerDateTime\"));\n        }\n\n        public void AddRecentPlayer(RecentPlayer recentPlayer)\n        {\n            IRCUser iu = connectionManager.UserList.Find(u => u.Name == recentPlayer.PlayerName);\n            bool isOnline = true;\n\n            if (iu == null)\n            {\n                iu = new IRCUser(recentPlayer.PlayerName);\n                isOnline = false;\n            }\n\n            var textColor = isOnline ? UISettings.ActiveSettings.AltColor : UISettings.ActiveSettings.DisabledItemColor;\n            AddItem(new List<XNAListBoxItem>()\n            {\n                new XNAListBoxItem(recentPlayer.PlayerName, textColor)\n                {\n                    Tag = iu\n                },\n                new XNAListBoxItem(recentPlayer.GameName, textColor),\n                new XNAListBoxItem(recentPlayer.GameTime.ToLocalTime().ToString(\"ddd, MMM d, yyyy @ h:mm tt\"), textColor)\n            });\n        }\n\n        private XNAPanel CreateColumnHeader(string headerText)\n        {\n            XNALabel xnaLabel = new XNALabel(WindowManager);\n            xnaLabel.FontIndex = HeaderFontIndex;\n            xnaLabel.X = 3;\n            xnaLabel.Y = 2;\n            xnaLabel.Text = headerText;\n            XNAPanel header = new XNAPanel(WindowManager);\n            header.Height = xnaLabel.Height + 3;\n            var width = Width / 3;\n            if (DrawListBoxBorders)\n                header.Width = width + 1;\n            else\n                header.Width = width;\n            header.AddChild(xnaLabel);\n\n            return header;\n        }\n\n        private void AddColumn(string headerText)\n        {\n            var header = CreateColumnHeader(headerText);\n            var xnaListBox = new XNAListBox(WindowManager);\n            xnaListBox.RightClick += ListBox_RightClick;\n            AddColumn(header, xnaListBox);\n        }\n\n        private void ListBox_RightClick(object sender, EventArgs e)\n        {\n            if (HoveredIndex < 0 || HoveredIndex >= ItemCount)\n                return;\n            \n            SelectedIndex = HoveredIndex;\n\n            var selectedItem = GetItem(0, SelectedIndex);\n            PlayerRightClick?.Invoke(this, new RecentPlayerTableRightClickEventArgs((IRCUser)selectedItem.Tag));\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTableRightClickEventArgs.cs",
    "content": "﻿using System;\nusing DTAClient.Online;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    public class RecentPlayerTableRightClickEventArgs : EventArgs\n    {\n        public IRCUser IrcUser { get; set; }\n\n        public RecentPlayerTableRightClickEventArgs(IRCUser ircUser)\n        {\n            IrcUser = ircUser;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs",
    "content": "﻿using DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A list box for listing CnCNet tunnel servers.\n    /// </summary>\n    class TunnelListBox : XNAMultiColumnListBox\n    {\n        private static readonly Dictionary<string, int> CountryCodeFlagOffsets = ParseCountryCodeFlagOffsets();\n        private const int FLAG_WIDTH = 16;\n        private const int FLAG_HEIGHT = 16;\n\n        public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager)\n        {\n            this.tunnelHandler = tunnelHandler;\n\n            tunnelHandler.TunnelsRefreshed += TunnelHandler_TunnelsRefreshed;\n            tunnelHandler.TunnelPinged += TunnelHandler_TunnelPinged;\n\n            SelectedIndexChanged += TunnelListBox_SelectedIndexChanged;\n\n            int headerHeight = (int)Renderer.GetTextDimensions(\"Name\", HeaderFontIndex).Y;\n\n            Width = 466;\n            Height = LineHeight * 12 + headerHeight + 3;\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n\n            using Stream flagsStream = Assembly.GetAssembly(typeof(GameCollection)).GetManifestResourceStream(\"DTAClient.Icons.flags16.png\");\n            var flagsPNG = SixLabors.ImageSharp.Image.Load(flagsStream);\n            flagsSpriteSheet = AssetLoader.TextureFromImage(flagsPNG);\n\n            var flagListBox = new FlagListBox(windowManager, tunnelHandler, flagsSpriteSheet);\n            flagListBox.FontIndex = FontIndex;\n            flagListBox.LineHeight = LineHeight;\n\n            var flagHeader = new XNAPanel(windowManager);\n            flagHeader.Width = 20;\n            flagHeader.Height = headerHeight + 3;\n\n            AddColumn(flagHeader, flagListBox);\n\n            AddColumn(\"Name\".L10N(\"Client:Main:NameHeader\"), 210);\n            AddColumn(\"Official\".L10N(\"Client:Main:OfficialHeader\"), 70);\n            AddColumn(\"Ping\".L10N(\"Client:Main:PingHeader\"), 76);\n            AddColumn(\"Players\".L10N(\"Client:Main:PlayersHeader\"), 90);\n            AllowRightClickUnselect = false;\n            AllowKeyboardInput = true;\n        }\n\n        public event EventHandler ListRefreshed;\n\n        private readonly TunnelHandler tunnelHandler;\n        private Texture2D flagsSpriteSheet;\n\n        private int bestTunnelIndex = 0;\n        private int lowestTunnelRating = int.MaxValue;\n\n        private bool isManuallySelectedTunnel;\n        private string manuallySelectedTunnelAddress;\n\n\n        /// <summary>\n        /// Selects a tunnel from the list with the given address.\n        /// </summary>\n        /// <param name=\"address\">The address of the tunnel server to select.</param>\n        public void SelectTunnel(string address)\n        {\n            int index = tunnelHandler.Tunnels.FindIndex(t => t.Address == address);\n            if (index > -1)\n            {\n                SelectedIndex = index;\n                isManuallySelectedTunnel = true;\n                manuallySelectedTunnelAddress = address;\n            }\n        }\n\n        /// <summary>\n        /// Gets whether or not a tunnel from the list with the given address is selected.\n        /// </summary>\n        /// <param name=\"address\">The address of the tunnel server</param>\n        /// <returns>True if tunnel with given address is selected, otherwise false.</returns>\n        public bool IsTunnelSelected(string address) =>\n            tunnelHandler.Tunnels.FindIndex(t => t.Address == address) == SelectedIndex;\n\n        private void TunnelHandler_TunnelsRefreshed(object sender, EventArgs e)\n        {\n            ClearItems();\n\n            int tunnelIndex = 0;\n\n            foreach (CnCNetTunnel tunnel in tunnelHandler.Tunnels)\n            {\n                List<string> info = new List<string>();\n\n                info.Add(\"\"); // Flag column\n                info.Add(tunnel.Name);\n                info.Add(Conversions.BooleanToString(tunnel.Official, BooleanStringStyle.YESNO));\n                if (tunnel.PingInMs < 0)\n                    info.Add(\"Unknown\".L10N(\"Client:Main:UnknownPing\"));\n                else\n                    info.Add(tunnel.PingInMs + \" ms\");\n                info.Add(tunnel.Clients + \" / \" + tunnel.MaxClients);\n\n                AddItem(info, true);\n\n                XNAListBoxItem flagItem = GetItem(0, tunnelIndex);\n                if (flagItem != null)\n                    flagItem.Tag = GetFlagRectangle(tunnel.CountryCode);\n\n                if ((tunnel.Official || tunnel.Recommended) && tunnel.PingInMs > -1)\n                {\n                    int rating = GetTunnelRating(tunnel);\n                    if (rating < lowestTunnelRating)\n                    {\n                        bestTunnelIndex = tunnelIndex;\n                        lowestTunnelRating = rating;\n                    }\n                }\n\n                tunnelIndex++;\n            }\n\n            if (tunnelHandler.Tunnels.Count > 0)\n            {\n                if (!isManuallySelectedTunnel)\n                {\n                    SelectedIndex = bestTunnelIndex;\n                    isManuallySelectedTunnel = false;\n                }\n                else\n                {\n                    int manuallySelectedIndex = tunnelHandler.Tunnels.FindIndex(t => t.Address == manuallySelectedTunnelAddress);\n\n                    if (manuallySelectedIndex == -1)\n                    {\n                        SelectedIndex = bestTunnelIndex;\n                        isManuallySelectedTunnel = false;\n                    }\n                    else\n                        SelectedIndex = manuallySelectedIndex;\n                }\n            }\n\n            ListRefreshed?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void TunnelHandler_TunnelPinged(int tunnelIndex)\n        {\n            XNAListBoxItem lbItem = GetItem(3, tunnelIndex);\n            CnCNetTunnel tunnel = tunnelHandler.Tunnels[tunnelIndex];\n\n            if (tunnel.PingInMs == -1)\n                lbItem.Text = \"Unknown\".L10N(\"Client:Main:UnknownPing\");\n            else\n            {\n                lbItem.Text = tunnel.PingInMs + \" ms\";\n                int rating = GetTunnelRating(tunnel);\n\n                if (isManuallySelectedTunnel)\n                    return;\n\n                if ((tunnel.Recommended || tunnel.Official) && rating < lowestTunnelRating)\n                {\n                    bestTunnelIndex = tunnelIndex;\n                    lowestTunnelRating = rating;\n                    SelectedIndex = tunnelIndex;\n                }\n            }\n        }\n\n        private int GetTunnelRating(CnCNetTunnel tunnel)\n        {\n            double usageRatio = (double)tunnel.Clients / tunnel.MaxClients;\n\n            if (usageRatio == 0)\n                usageRatio = 0.1;\n\n            usageRatio *= 100.0;\n\n            return Convert.ToInt32(Math.Pow(tunnel.PingInMs, 2.0) * usageRatio);\n        }\n\n        private void TunnelListBox_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (!IsValidIndexSelected())\n                return;\n\n            isManuallySelectedTunnel = true;\n            manuallySelectedTunnelAddress = tunnelHandler.Tunnels[SelectedIndex].Address;\n        }\n\n        private static Dictionary<string, int> ParseCountryCodeFlagOffsets()\n        {\n            // 16px version from\n            // https://github.com/lafeber/world-flags-sprite\n            // Offsets are located in the css files.\n            return new Dictionary<string, int>\n            {\n                [\"ad\"] = 352,  [\"ae\"] = 368,  [\"af\"] = 384,  [\"ag\"] = 400,\n                [\"ai\"] = 416,  [\"al\"] = 432,  [\"am\"] = 448,  [\"ao\"] = 464,\n                [\"aq\"] = 480,  [\"ar\"] = 496,  [\"as\"] = 512,  [\"at\"] = 528,\n                [\"au\"] = 544,  [\"aw\"] = 560,  [\"ax\"] = 576,  [\"az\"] = 592,\n                [\"ba\"] = 608,  [\"bb\"] = 624,  [\"bd\"] = 640,  [\"be\"] = 656,\n                [\"bf\"] = 672,  [\"bg\"] = 688,  [\"bh\"] = 704,  [\"bi\"] = 720,\n                [\"bj\"] = 736,  [\"bl\"] = 1424, [\"bm\"] = 752,  [\"bn\"] = 768,\n                [\"bo\"] = 784,  [\"bq\"] = 2752, [\"br\"] = 800,  [\"bs\"] = 816,\n                [\"bt\"] = 832,  [\"bv\"] = 2768, [\"bw\"] = 848,  [\"by\"] = 864,\n                [\"bz\"] = 880,  [\"ca\"] = 896,  [\"cd\"] = 912,  [\"cf\"] = 928,\n                [\"cg\"] = 944,  [\"ch\"] = 960,  [\"ci\"] = 976,  [\"ck\"] = 992,\n                [\"cl\"] = 1008, [\"cm\"] = 1024, [\"cn\"] = 1040, [\"co\"] = 1056,\n                [\"cp\"] = 1424, [\"cr\"] = 1072, [\"cu\"] = 1088, [\"cv\"] = 1104,\n                [\"cw\"] = 3920, [\"cy\"] = 1120, [\"cz\"] = 1136, [\"de\"] = 1152,\n                [\"dj\"] = 1168, [\"dk\"] = 1184, [\"dm\"] = 1200, [\"do\"] = 1216,\n                [\"dz\"] = 1232, [\"ec\"] = 1248, [\"ee\"] = 1264, [\"eg\"] = 1280,\n                [\"eh\"] = 1296, [\"er\"] = 1312, [\"es\"] = 1328, [\"et\"] = 1344,\n                [\"fi\"] = 1360, [\"fj\"] = 1376, [\"fm\"] = 1392, [\"fo\"] = 1408,\n                [\"fr\"] = 1424, [\"ga\"] = 1440, [\"gb\"] = 1456, [\"gd\"] = 1472,\n                [\"ge\"] = 1488, [\"gg\"] = 1504, [\"gh\"] = 1520, [\"gi\"] = 1536,\n                [\"gl\"] = 1552, [\"gm\"] = 1568, [\"gn\"] = 1584, [\"gp\"] = 1600,\n                [\"gq\"] = 1616, [\"gr\"] = 1632, [\"gt\"] = 1648, [\"gu\"] = 1664,\n                [\"gw\"] = 1680, [\"gy\"] = 1696, [\"hk\"] = 1712, [\"hn\"] = 1728,\n                [\"hr\"] = 1744, [\"ht\"] = 1760, [\"hu\"] = 1776, [\"id\"] = 1792,\n                [\"ie\"] = 1808, [\"il\"] = 1824, [\"im\"] = 1840, [\"in\"] = 1856,\n                [\"iq\"] = 1872, [\"ir\"] = 1888, [\"is\"] = 1904, [\"it\"] = 1920,\n                [\"je\"] = 1936, [\"jm\"] = 1952, [\"jo\"] = 1968, [\"jp\"] = 1984,\n                [\"ke\"] = 2000, [\"kg\"] = 2016, [\"kh\"] = 2032, [\"ki\"] = 2048,\n                [\"km\"] = 2064, [\"kn\"] = 2080, [\"kp\"] = 2096, [\"kr\"] = 2112,\n                [\"kw\"] = 2128, [\"ky\"] = 2144, [\"kz\"] = 2160, [\"la\"] = 2176,\n                [\"lb\"] = 2192, [\"lc\"] = 2208, [\"li\"] = 2224, [\"lk\"] = 2240,\n                [\"lr\"] = 2256, [\"ls\"] = 2272, [\"lt\"] = 2288, [\"lu\"] = 2304,\n                [\"lv\"] = 2320, [\"ly\"] = 2336, [\"ma\"] = 2352, [\"mc\"] = 1792,\n                [\"md\"] = 2368, [\"me\"] = 2384, [\"mf\"] = 1424, [\"mg\"] = 2400,\n                [\"mh\"] = 2416, [\"mk\"] = 2432, [\"ml\"] = 2448, [\"mm\"] = 2464,\n                [\"mn\"] = 2480, [\"mo\"] = 2496, [\"mq\"] = 2512, [\"mr\"] = 2528,\n                [\"ms\"] = 2544, [\"mt\"] = 2560, [\"mu\"] = 2576, [\"mv\"] = 2592,\n                [\"mw\"] = 2608, [\"mx\"] = 2624, [\"my\"] = 2640, [\"mz\"] = 2656,\n                [\"na\"] = 2672, [\"nc\"] = 2688, [\"ne\"] = 2704, [\"ng\"] = 2720,\n                [\"ni\"] = 2736, [\"nl\"] = 2752, [\"no\"] = 2768, [\"np\"] = 2784,\n                [\"nq\"] = 2768, [\"nr\"] = 2800, [\"nu\"] = 3952, [\"nz\"] = 2816,\n                [\"om\"] = 2832, [\"pa\"] = 2848, [\"pe\"] = 2864, [\"pf\"] = 2880,\n                [\"pg\"] = 2896, [\"ph\"] = 2912, [\"pk\"] = 2928, [\"pl\"] = 2944,\n                [\"pr\"] = 2960, [\"ps\"] = 2976, [\"pt\"] = 2992, [\"pw\"] = 3008,\n                [\"py\"] = 3024, [\"qa\"] = 3040, [\"re\"] = 3056, [\"ro\"] = 3072,\n                [\"rs\"] = 3088, [\"ru\"] = 3104, [\"rw\"] = 3120, [\"sa\"] = 3136,\n                [\"sb\"] = 3152, [\"sc\"] = 3168, [\"sd\"] = 3184, [\"se\"] = 3200,\n                [\"sg\"] = 3216, [\"sh\"] = 1456, [\"si\"] = 3232, [\"sj\"] = 2768,\n                [\"sk\"] = 3248, [\"sl\"] = 3264, [\"sm\"] = 3280, [\"sn\"] = 3296,\n                [\"so\"] = 3312, [\"sr\"] = 3328, [\"ss\"] = 3936, [\"st\"] = 3344,\n                [\"sv\"] = 3360, [\"sx\"] = 3904, [\"sy\"] = 3376, [\"sz\"] = 3392,\n                [\"tc\"] = 3408, [\"td\"] = 3424, [\"tg\"] = 3440, [\"th\"] = 3456,\n                [\"tj\"] = 3472, [\"tl\"] = 3488, [\"tm\"] = 3504, [\"tn\"] = 3520,\n                [\"to\"] = 3536, [\"tr\"] = 3552, [\"tt\"] = 3568, [\"tv\"] = 3584,\n                [\"tw\"] = 3600, [\"tz\"] = 3616, [\"ua\"] = 3632, [\"ug\"] = 3648,\n                [\"us\"] = 3664, [\"uy\"] = 3680, [\"uz\"] = 3696, [\"va\"] = 3712,\n                [\"vc\"] = 3728, [\"ve\"] = 3744, [\"vg\"] = 3760, [\"vi\"] = 3776,\n                [\"vn\"] = 3792, [\"vu\"] = 3808, [\"ws\"] = 3824, [\"ye\"] = 3840,\n                [\"yt\"] = 1424, [\"za\"] = 3856, [\"zm\"] = 3872, [\"zw\"] = 3888\n            };\n        }\n\n        private static Rectangle? GetFlagRectangle(string countryCode)\n        {\n            if (string.IsNullOrEmpty(countryCode))\n                return null;\n\n            string code = countryCode.ToLowerInvariant();\n            if (CountryCodeFlagOffsets.TryGetValue(code, out int yOffset))\n            {\n                return new Rectangle(0, yOffset, FLAG_WIDTH, FLAG_HEIGHT);\n            }\n\n            return null;\n        }\n\n        /// <summary>\n        /// Custom listbox that draws country flags.\n        /// </summary>\n        private class FlagListBox : XNAListBox\n        {\n            private readonly TunnelHandler tunnelHandler;\n            private readonly Texture2D flagsSpriteSheet;\n\n            public FlagListBox(WindowManager windowManager, TunnelHandler tunnelHandler, Texture2D flagsSpriteSheet)\n                : base(windowManager)\n            {\n                this.tunnelHandler = tunnelHandler;\n                this.flagsSpriteSheet = flagsSpriteSheet;\n            }\n\n            public override void Draw(GameTime gameTime)\n            {\n                DrawPanel();\n\n                int height = 2 - (ViewTop % LineHeight);\n\n                for (int i = TopIndex; i < Items.Count; i++)\n                {\n                    if (height > Height)\n                        break;\n\n                    Rectangle? flagRect = Items[i].Tag as Rectangle?;\n\n                    if (flagRect.HasValue)\n                    {\n                        int x = (Width - FLAG_WIDTH) / 2;\n                        DrawTexture(flagsSpriteSheet,\n                            flagRect.Value,\n                            new Rectangle(x, height, FLAG_WIDTH, FLAG_HEIGHT),\n                            Color.White);\n                    }\n\n                    height += LineHeight;\n                }\n\n                if (DrawBorders)\n                    DrawPanelBorders();\n\n                DrawChildren(gameTime);\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs",
    "content": "﻿using ClientGUI;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A window for selecting a CnCNet tunnel server.\n    /// </summary>\n    class TunnelSelectionWindow : XNAWindow\n    {\n        public TunnelSelectionWindow(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager)\n        {\n            this.tunnelHandler = tunnelHandler;\n        }\n\n        public event EventHandler<TunnelEventArgs> TunnelSelected;\n\n        private readonly TunnelHandler tunnelHandler;\n        private TunnelListBox lbTunnelList;\n        private XNALabel lblDescription;\n        private XNAClientButton btnApply;\n\n        private string originalTunnelAddress;\n\n        public override void Initialize()\n        {\n            if (Initialized)\n                return;\n\n            Name = \"TunnelSelectionWindow\";\n\n            BackgroundTexture = AssetLoader.LoadTexture(\"gamecreationoptionsbg.png\");\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.Text = \"Line 1\" + Environment.NewLine + \"Line 2\";\n            lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP + UIDesignConstants.CONTROL_VERTICAL_MARGIN;\n            AddChild(lblDescription);\n\n            lbTunnelList = new TunnelListBox(WindowManager, tunnelHandler);\n            lbTunnelList.Name = nameof(lbTunnelList);\n            lbTunnelList.Y = lblDescription.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN;\n            lbTunnelList.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            AddChild(lbTunnelList);\n            lbTunnelList.SelectedIndexChanged += LbTunnelList_SelectedIndexChanged;\n\n            btnApply = new XNAClientButton(WindowManager);\n            btnApply.Name = nameof(btnApply);\n            btnApply.Width = UIDesignConstants.BUTTON_WIDTH_92;\n            btnApply.Height = UIDesignConstants.BUTTON_HEIGHT;\n            btnApply.Text = \"Apply\".L10N(\"Client:Main:ButtonApply\");\n            btnApply.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n            btnApply.Y = lbTunnelList.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3;\n            AddChild(btnApply);\n            btnApply.LeftClick += BtnApply_LeftClick;\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.Width = UIDesignConstants.BUTTON_WIDTH_92;\n            btnCancel.Height = UIDesignConstants.BUTTON_HEIGHT;\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.Y = btnApply.Y;\n            AddChild(btnCancel);\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            Width = lbTunnelList.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN + UIDesignConstants.EMPTY_SPACE_SIDES;\n            Height = btnApply.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n            btnCancel.X = Width - btnCancel.Width - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN;\n\n            base.Initialize();\n        }\n\n        private void BtnApply_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n\n            if (!lbTunnelList.IsValidIndexSelected())\n                return;\n\n            CnCNetTunnel tunnel = tunnelHandler.Tunnels[lbTunnelList.SelectedIndex];\n            TunnelSelected?.Invoke(this, new TunnelEventArgs(tunnel));\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e) => Disable();\n\n        private void LbTunnelList_SelectedIndexChanged(object sender, EventArgs e) =>\n            btnApply.AllowClick = !lbTunnelList.IsTunnelSelected(originalTunnelAddress) && lbTunnelList.IsValidIndexSelected();\n\n        /// <summary>\n        /// Sets the window's description and selects the tunnel server\n        /// with the given address.\n        /// </summary>\n        /// <param name=\"description\">The window description.</param>\n        /// <param name=\"tunnelAddress\">The address of the tunnel server to select.</param>\n        public void Open(string description, string tunnelAddress = null)\n        {\n            lblDescription.Text = description;\n            originalTunnelAddress = tunnelAddress;\n\n            if (!string.IsNullOrWhiteSpace(tunnelAddress))\n                lbTunnelList.SelectTunnel(tunnelAddress);\n            else\n                lbTunnelList.SelectedIndex = -1;\n\n            if (lbTunnelList.SelectedIndex > -1)\n            {\n                lbTunnelList.SetTopIndex(0);\n\n                while (lbTunnelList.SelectedIndex > lbTunnelList.LastIndex)\n                    lbTunnelList.TopIndex++;\n            }\n\n            btnApply.AllowClick = false;\n            Enable();\n        }\n    }\n\n    class TunnelEventArgs : EventArgs\n    {\n        public TunnelEventArgs(CnCNetTunnel tunnel)\n        {\n            Tunnel = tunnel;\n        }\n\n        public CnCNetTunnel Tunnel { get; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameFiltersPanel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing ClientCore;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// Custom scroll panel that exposes ContentPanel for adding children\n    /// </summary>\n    internal class GameFiltersScrollPanel : XNAScrollPanel\n    {\n        public GameFiltersScrollPanel(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public XNAPanel GetContentPanel() => ContentPanel;\n    }\n\n    public class GameFiltersPanel : XNAPanel\n    {\n        private const int minPlayerCount = 2;\n        private const int maxPlayerCount = 8;\n        private const int GAP = 12;\n        private const int BOTTOM_PANEL_HEIGHT = 60;\n\n        private GameFiltersScrollPanel scrollPanel;\n        private XNAPanel bottomPanel;\n\n        private XNAClientCheckBox chkBoxFriendsOnly;\n        private XNAClientCheckBox chkBoxHideLockedGames;\n        private XNAClientCheckBox chkBoxHidePasswordedGames;\n        private XNAClientCheckBox chkBoxHideIncompatibleGames;\n        private XNAClientDropDown ddMaxPlayerCount;\n\n        private GameLobbyBase gameLobby;\n        private List<GameOptionFilterControl> gameOptionFilterControls = [];\n        private bool gameOptionFiltersCreated = false;\n\n        private class GameOptionFilterControl\n        {\n            public string OptionName { get; set; }\n            public bool IsCheckbox { get; set; }\n            public XNAClientDropDown DropDown { get; set; }\n            public XNALabel Label { get; set; }\n            public XNAPanel IconPanel { get; set; }\n            public string EnabledIcon { get; set; }\n            public string DisabledIcon { get; set; }\n        }\n\n        public GameFiltersPanel(WindowManager windowManager, GameLobbyBase gameLobby) : base(windowManager)\n        {\n            this.gameLobby = gameLobby;\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            Name = \"GameFiltersWindow\";\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0), Width, Height);\n\n            // Create scroll panel for filters content\n            scrollPanel = new GameFiltersScrollPanel(WindowManager);\n            scrollPanel.Name = \"FiltersScrollPanel\";\n            scrollPanel.ClientRectangle = new Rectangle(0, 0, Width, Height - BOTTOM_PANEL_HEIGHT);\n            scrollPanel.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 200), 1, 1);\n            scrollPanel.DrawBorders = false;\n\n            // Create bottom panel for Save/Cancel buttons\n            bottomPanel = new XNAPanel(WindowManager);\n            bottomPanel.Name = \"BottomButtonPanel\";\n            bottomPanel.ClientRectangle = new Rectangle(0, Height - BOTTOM_PANEL_HEIGHT, Width, BOTTOM_PANEL_HEIGHT);\n            bottomPanel.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1);\n            bottomPanel.DrawBorders = false;\n\n            var lblTitle = new XNALabel(WindowManager);\n            lblTitle.Name = nameof(lblTitle);\n            lblTitle.Text = \"Game Filters\".L10N(\"Client:Main:GameFilters\");\n            lblTitle.ClientRectangle = new Rectangle(\n                GAP, GAP, 120, UIDesignConstants.BUTTON_HEIGHT\n            );\n\n            chkBoxFriendsOnly = new XNAClientCheckBox(WindowManager);\n            chkBoxFriendsOnly.Name = nameof(chkBoxFriendsOnly);\n            chkBoxFriendsOnly.Text = \"Show Friend Games Only\".L10N(\"Client:Main:FriendGameOnly\");\n            chkBoxFriendsOnly.ClientRectangle = new Rectangle(\n                GAP, lblTitle.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                0, 0\n            );\n\n            chkBoxHideLockedGames = new XNAClientCheckBox(WindowManager);\n            chkBoxHideLockedGames.Name = nameof(chkBoxHideLockedGames);\n            chkBoxHideLockedGames.Text = \"Hide Locked Games\".L10N(\"Client:Main:HideLockedGame\");\n            chkBoxHideLockedGames.ClientRectangle = new Rectangle(\n                GAP, chkBoxFriendsOnly.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                0, 0\n            );\n\n            chkBoxHidePasswordedGames = new XNAClientCheckBox(WindowManager);\n            chkBoxHidePasswordedGames.Name = nameof(chkBoxHidePasswordedGames);\n            chkBoxHidePasswordedGames.Text = \"Hide Passworded Games\".L10N(\"Client:Main:HidePasswordGame\");\n            chkBoxHidePasswordedGames.ClientRectangle = new Rectangle(\n                GAP, chkBoxHideLockedGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                0, 0\n            );\n\n            chkBoxHideIncompatibleGames = new XNAClientCheckBox(WindowManager);\n            chkBoxHideIncompatibleGames.Name = nameof(chkBoxHideIncompatibleGames);\n            chkBoxHideIncompatibleGames.Text = \"Hide Incompatible Games\".L10N(\"Client:Main:HideIncompatibleGame\");\n            chkBoxHideIncompatibleGames.ClientRectangle = new Rectangle(\n                GAP, chkBoxHidePasswordedGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                0, 0\n            );\n\n            ddMaxPlayerCount = new XNAClientDropDown(WindowManager);\n            ddMaxPlayerCount.Name = nameof(ddMaxPlayerCount);\n            ddMaxPlayerCount.ClientRectangle = new Rectangle(\n                GAP, chkBoxHideIncompatibleGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                40, UIDesignConstants.BUTTON_HEIGHT\n            );\n            for (int i = minPlayerCount; i <= maxPlayerCount; i++)\n            {\n                ddMaxPlayerCount.AddItem(i.ToString());\n            }\n\n            var lblMaxPlayerCount = new XNALabel(WindowManager);\n            lblMaxPlayerCount.Name = nameof(lblMaxPlayerCount);\n            lblMaxPlayerCount.Text = \"Max Player Count\".L10N(\"Client:Main:MaxPlayerCount\");\n            lblMaxPlayerCount.ClientRectangle = new Rectangle(\n                ddMaxPlayerCount.X + ddMaxPlayerCount.Width + GAP, ddMaxPlayerCount.Y,\n                0, UIDesignConstants.BUTTON_HEIGHT\n            );\n\n            var btnResetDefaults = new XNAClientButton(WindowManager);\n            btnResetDefaults.Name = nameof(btnResetDefaults);\n            btnResetDefaults.Text = \"Reset Defaults\".L10N(\"Client:Main:ResetDefaults\");\n            btnResetDefaults.ClientRectangle = new Rectangle(\n                GAP, ddMaxPlayerCount.Y + UIDesignConstants.BUTTON_HEIGHT + GAP,\n                UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT\n            );\n            btnResetDefaults.LeftClick += BtnResetDefaults_LeftClick;\n\n            var btnSave = new XNAClientButton(WindowManager);\n            btnSave.Name = nameof(btnSave);\n            btnSave.Text = \"Save\".L10N(\"Client:Main:ButtonSave\");\n            btnSave.ClientRectangle = new Rectangle(\n                GAP, (BOTTOM_PANEL_HEIGHT - UIDesignConstants.BUTTON_HEIGHT) / 2,\n                UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT\n            );\n            btnSave.LeftClick += BtnSave_LeftClick;\n\n            var btnCancel = new XNAClientButton(WindowManager);\n            btnCancel.Name = nameof(btnCancel);\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.ClientRectangle = new Rectangle(\n                Width - GAP - UIDesignConstants.BUTTON_WIDTH_92, (BOTTOM_PANEL_HEIGHT - UIDesignConstants.BUTTON_HEIGHT) / 2,\n                UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT\n            );\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            scrollPanel.GetContentPanel().AddChild(lblTitle);\n            scrollPanel.GetContentPanel().AddChild(chkBoxFriendsOnly);\n            scrollPanel.GetContentPanel().AddChild(chkBoxHideLockedGames);\n            scrollPanel.GetContentPanel().AddChild(chkBoxHidePasswordedGames);\n            scrollPanel.GetContentPanel().AddChild(chkBoxHideIncompatibleGames);\n            scrollPanel.GetContentPanel().AddChild(lblMaxPlayerCount);\n            scrollPanel.GetContentPanel().AddChild(ddMaxPlayerCount);\n            scrollPanel.GetContentPanel().AddChild(btnResetDefaults);\n\n            bottomPanel.AddChild(btnSave);\n            bottomPanel.AddChild(btnCancel);\n\n            AddChild(scrollPanel);\n            AddChild(bottomPanel);\n        }\n\n        private void CreateGameOptionFilters()\n        {\n            // Note: broadcasted checkboxes are converted to dropdowns so we can have a third - undefined - value.\n\n            if (gameLobby == null)\n                return;\n\n            var broadcastableCheckboxes = gameLobby.CheckBoxes.Where(cb => cb.BroadcastToLobby && cb.ShowInFilters).ToList();\n            var broadcastableDropdowns = gameLobby.DropDowns.Where(dd => dd.BroadcastToLobby && dd.ShowInFilters).ToList();\n\n            if (broadcastableCheckboxes.Count == 0 && broadcastableDropdowns.Count == 0)\n                return;\n\n            int currentY = ddMaxPlayerCount.Y + UIDesignConstants.BUTTON_HEIGHT + GAP;\n            const int iconLabelSpacing = 6;\n            const int itemVerticalSpacing = 4;\n            const int minLabelRowHeight = 18;\n\n            int dropdownWidth = (scrollPanel.Width - (GAP * 3)) / 2;\n\n            var divider = CreateDivider(currentY, scrollPanel.Width);\n            scrollPanel.GetContentPanel().AddChild(divider);\n            currentY += divider.Height + GAP;\n\n            int leftColumnX = GAP;\n            int rightColumnX = scrollPanel.Width / 2 + GAP / 2;\n            int filterIndex = 0;\n            int maxItemHeight = 0;\n\n            // Create filters for broadcastable checkboxes\n            foreach (var checkbox in broadcastableCheckboxes)\n            {\n                var filterControl = new GameOptionFilterControl\n                {\n                    OptionName = checkbox.Name,\n                    IsCheckbox = true,\n                    EnabledIcon = checkbox.EnabledIcon,\n                    DisabledIcon = checkbox.DisabledIcon\n                };\n\n                Texture2D icon = null;\n                if (!string.IsNullOrEmpty(checkbox.EnabledIcon))\n                    icon = AssetLoader.LoadTexture(checkbox.EnabledIcon);\n\n                int iconWidth = icon?.Width ?? 0;\n                int iconHeight = icon?.Height ?? 0;\n\n                bool isLeftColumn = (filterIndex % 2 == 0);\n                int columnX = isLeftColumn ? leftColumnX : rightColumnX;\n                int rowY = currentY + (filterIndex / 2) * maxItemHeight;\n\n                XNAPanel iconPanel = null;\n                if (icon != null)\n                {\n                    iconPanel = new XNAPanel(WindowManager)\n                    {\n                        Name = $\"icon{checkbox.Name}Filter\",\n                        ClientRectangle = new Rectangle(columnX, rowY, iconWidth, iconHeight),\n                        DrawBorders = false,\n                        BackgroundTexture = icon\n                    };\n                    filterControl.IconPanel = iconPanel;\n                }\n\n                var label = new XNALabel(WindowManager)\n                {\n                    Name = $\"lbl{checkbox.Name}Filter\",\n                    Text = checkbox.Text + \":\",\n                    ClientRectangle = new Rectangle(\n                    columnX + iconWidth + (iconWidth > 0 ? iconLabelSpacing : 0), rowY,\n                    0, UIDesignConstants.BUTTON_HEIGHT)\n                };\n\n                int topRowHeight = Math.Max(iconHeight, minLabelRowHeight);\n\n                var dropdown = new XNAClientDropDown(WindowManager)\n                {\n                    Name = $\"dd{checkbox.Name}Filter\",\n                    ClientRectangle = new Rectangle(columnX, rowY + topRowHeight + itemVerticalSpacing, dropdownWidth, UIDesignConstants.BUTTON_HEIGHT)\n                };\n\n                // \"All\" item has no icon\n                dropdown.AddItem(new XNADropDownItem { Text = \"All\".L10N(\"Client:Main:FilterAllGames\"), Texture = null });\n\n                Texture2D enabledIconTexture = null;\n                if (!string.IsNullOrEmpty(checkbox.EnabledIcon))\n                    enabledIconTexture = AssetLoader.LoadTexture(checkbox.EnabledIcon);\n                dropdown.AddItem(new XNADropDownItem { Text = \"On\".L10N(\"Client:Main:FilterOn\"), Texture = enabledIconTexture });\n\n                Texture2D disabledIconTexture = null;\n                if (!string.IsNullOrEmpty(checkbox.DisabledIcon))\n                    disabledIconTexture = AssetLoader.LoadTexture(checkbox.DisabledIcon);\n                dropdown.AddItem(new XNADropDownItem { Text = \"Off\".L10N(\"Client:Main:FilterOff\"), Texture = disabledIconTexture });\n\n                dropdown.SelectedIndex = 0;\n\n                filterControl.DropDown = dropdown;\n                filterControl.Label = label;\n\n                int itemHeight = topRowHeight + itemVerticalSpacing + UIDesignConstants.BUTTON_HEIGHT + GAP;\n                if (itemHeight > maxItemHeight)\n                    maxItemHeight = itemHeight;\n\n                gameOptionFilterControls.Add(filterControl);\n                if (iconPanel != null)\n                    scrollPanel.GetContentPanel().AddChild(iconPanel);\n                scrollPanel.GetContentPanel().AddChild(dropdown);\n                scrollPanel.GetContentPanel().AddChild(label);\n\n                filterIndex++;\n            }\n\n            // Create filters for broadcastable dropdowns\n            foreach (var lobbyDropdown in broadcastableDropdowns)\n            {\n                var filterControl = new GameOptionFilterControl\n                {\n                    OptionName = lobbyDropdown.Name,\n                    IsCheckbox = false\n                };\n\n                // For dropdowns with multiple icons, show the first one initially\n                Texture2D icon = lobbyDropdown.Items.Count > 0 ? lobbyDropdown.Items[0].Texture : null;\n\n                int iconWidth = icon?.Width ?? 0;\n                int iconHeight = icon?.Height ?? 0;\n\n                bool isLeftColumn = (filterIndex % 2 == 0);\n                int columnX = isLeftColumn ? leftColumnX : rightColumnX;\n                int rowY = currentY + (filterIndex / 2) * maxItemHeight;\n\n                XNAPanel iconPanel = null;\n                if (icon != null)\n                {\n                    iconPanel = new XNAPanel(WindowManager)\n                    {\n                        Name = $\"icon{lobbyDropdown.Name}Filter\",\n                        ClientRectangle = new Rectangle(columnX, rowY, iconWidth, iconHeight),\n                        DrawBorders = false,\n                        BackgroundTexture = icon\n                    };\n                    filterControl.IconPanel = iconPanel;\n                }\n\n                var label = new XNALabel(WindowManager)\n                {\n                    Name = $\"lbl{lobbyDropdown.Name}Filter\",\n                    Text = lobbyDropdown.OptionName,\n                    ClientRectangle = new Rectangle(\n                    columnX + iconWidth + (iconWidth > 0 ? iconLabelSpacing : 0), rowY,\n                    0, UIDesignConstants.BUTTON_HEIGHT)\n                };\n\n                int topRowHeight = Math.Max(iconHeight, minLabelRowHeight);\n\n                var dropdown = new XNAClientDropDown(WindowManager)\n                {\n                    Name = $\"dd{lobbyDropdown.Name}Filter\",\n                    ClientRectangle = new Rectangle(columnX, rowY + topRowHeight + itemVerticalSpacing, dropdownWidth, UIDesignConstants.BUTTON_HEIGHT)\n                };\n\n                dropdown.AddItem(new XNADropDownItem { Text = \"All\".L10N(\"Client:Main:FilterAllGames\"), Texture = null });\n\n                for (int i = 0; i < lobbyDropdown.Items.Count; i++)\n                {\n                    var item = lobbyDropdown.Items[i];\n                    dropdown.AddItem(new XNADropDownItem { Text = item.Text, Tag = item.Tag, Texture = item.Texture });\n                }\n\n                dropdown.SelectedIndex = 0;\n\n                filterControl.DropDown = dropdown;\n                filterControl.Label = label;\n\n                int itemHeight = topRowHeight + itemVerticalSpacing + UIDesignConstants.BUTTON_HEIGHT + GAP;\n                if (itemHeight > maxItemHeight)\n                    maxItemHeight = itemHeight;\n\n                gameOptionFilterControls.Add(filterControl);\n                if (iconPanel != null)\n                    scrollPanel.GetContentPanel().AddChild(iconPanel);\n                scrollPanel.GetContentPanel().AddChild(dropdown);\n                scrollPanel.GetContentPanel().AddChild(label);\n\n                filterIndex++;\n            }\n\n            if (gameOptionFilterControls.Count > 0)\n            {\n                int numRows = (filterIndex + 1) / 2;\n                currentY += numRows * maxItemHeight;\n\n                var secondDivider = CreateDivider(currentY, scrollPanel.Width);\n                scrollPanel.GetContentPanel().AddChild(secondDivider);\n\n                currentY += secondDivider.Height + GAP;\n\n                var btnResetDefaults = scrollPanel.GetContentPanel().Children.FirstOrDefault(c => c.Name == \"btnResetDefaults\") as XNAClientButton;\n                if (btnResetDefaults != null)\n                {\n                    btnResetDefaults.ClientRectangle = new Rectangle(\n                        GAP, currentY,\n                        UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT\n                    );\n                }\n                UpdateScrollContentHeight();\n            }\n        }\n\n        private void BtnSave_LeftClick(object sender, EventArgs e)\n        {\n            Save();\n            Disable();\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Cancel();\n        }\n\n        private void BtnResetDefaults_LeftClick(object sender, EventArgs e)\n        {\n            ResetDefaults();\n        }\n\n        private void Save()\n        {\n            var userIniSettings = UserINISettings.Instance;\n            userIniSettings.ShowFriendGamesOnly.Value = chkBoxFriendsOnly.Checked;\n            userIniSettings.HideLockedGames.Value = chkBoxHideLockedGames.Checked;\n            userIniSettings.HidePasswordedGames.Value = chkBoxHidePasswordedGames.Checked;\n            userIniSettings.HideIncompatibleGames.Value = chkBoxHideIncompatibleGames.Checked;\n            userIniSettings.MaxPlayerCount.Value = int.Parse(ddMaxPlayerCount.SelectedItem.Text);\n\n            // Save game option filters (only non-default values)\n            var gameOptionFiltersSection = userIniSettings.SettingsIni.GetSection(UserINISettings.GAME_OPTION_FILTERS);\n            gameOptionFiltersSection?.RemoveAllKeys();\n\n            foreach (var filterControl in gameOptionFilterControls)\n            {\n                if (filterControl.IsCheckbox)\n                {\n                    // UI: 0 = All, 1 = On, 2 = Off\n                    // Storage: null = All, 1 = On, 0 = Off\n                    int? filterValue = filterControl.DropDown.SelectedIndex switch\n                    {\n                        0 => null,   // All\n                        1 => 1,      // On\n                        2 => 0,      // Off\n                        _ => null\n                    };\n                    if (filterValue != null) // Only save if not \"All\"\n                        userIniSettings.SetGameOptionFilterValue(filterControl.OptionName, filterValue);\n                }\n                else\n                {\n                    // UI: 0 = All, 1+ = game option indices\n                    // Storage: null = All, otherwise actual index\n                    int? filterValue = filterControl.DropDown.SelectedIndex == 0 ? null : filterControl.DropDown.SelectedIndex - 1;\n                    if (filterValue != null) // if not \"All\"\n                        userIniSettings.SetGameOptionFilterValue(filterControl.OptionName, filterValue);\n                }\n            }\n\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        private void Load()\n        {\n            var userIniSettings = UserINISettings.Instance;\n            chkBoxFriendsOnly.Checked = userIniSettings.ShowFriendGamesOnly.Value;\n            chkBoxHideLockedGames.Checked = userIniSettings.HideLockedGames.Value;\n            chkBoxHidePasswordedGames.Checked = userIniSettings.HidePasswordedGames.Value;\n            chkBoxHideIncompatibleGames.Checked = userIniSettings.HideIncompatibleGames.Value;\n            ddMaxPlayerCount.SelectedIndex = ddMaxPlayerCount.Items.FindIndex(i => i.Text == userIniSettings.MaxPlayerCount.Value.ToString());\n\n            foreach (var filterControl in gameOptionFilterControls)\n            {\n                int? filterValue = userIniSettings.GetGameOptionFilterValue(filterControl.OptionName);\n\n                if (filterControl.IsCheckbox)\n                {\n                    // Storage: null = All, 1 = On, 0 = Off\n                    // UI: 0 = All, 1 = On, 2 = Off\n                    filterControl.DropDown.SelectedIndex = filterValue switch\n                    {\n                        null => 0,   // All\n                        1 => 1,      // On\n                        0 => 2,      // Off\n                        _ => 0\n                    };\n                }\n                else\n                {\n                    // Storage: null = All, otherwise actual index\n                    // UI: 0 = All, 1+ = game option indices\n                    filterControl.DropDown.SelectedIndex = filterValue == null ? 0 : filterValue.Value + 1;\n                }\n            }\n        }\n\n        private void ResetDefaults()\n        {\n            UserINISettings.Instance.ResetGameFilters();\n            Load();\n        }\n\n        public void Show()\n        {\n            if (!gameOptionFiltersCreated)\n            {\n                CreateGameOptionFilters();\n                gameOptionFiltersCreated = true;\n            }\n\n            Load();\n            Enable();\n        }\n\n        public void Cancel()\n        {\n            Disable();\n        }\n        private void UpdateScrollContentHeight()\n        {\n            var content = scrollPanel.GetContentPanel();\n            int bottom = content.Children.Max(c => c.Bottom);\n            content.Height = bottom + GAP;\n        }\n\n        private XNAPanel CreateDivider(int y, int width, int height = 1)\n        {\n            var dividerPanel = new XNAPanel(WindowManager)\n            {\n                DrawBorders = true,\n                ClientRectangle = new Rectangle(0, y, width, height)\n            };\n            return dividerPanel;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameInformationIconOnlyPanel.cs",
    "content": "using Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    ///<summary>\n    /// A panel for showing a game information icon without text.\n    ///</summary>\n    public class GameInformationIconOnlyPanel : XNAPanel\n    {\n        private readonly Texture2D icon;\n\n        public GameInformationIconOnlyPanel(WindowManager windowManager, Texture2D icon) : base(windowManager)\n        {\n            this.icon = icon;\n            DrawBorders = false;\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            base.Draw(gameTime);\n\n            if (icon == null)\n                return;\n\n            DrawTexture(icon, new Rectangle(0, 0, icon.Width, icon.Height), Color.White);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameInformationIconPanel.cs",
    "content": "﻿using System;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    ///<summary>\n    /// A panel for showing a game information icon next to its associated label.\n    ///</summary>\n    public class GameInformationIconPanel : XNAPanel\n    {\n        private readonly Texture2D icon;\n        private readonly string label;\n        private readonly int maxIconWidth;\n        private const int iconLabelSpacing = 6;\n        public int FontIndex = 0;\n\n        public GameInformationIconPanel(WindowManager windowManager, Texture2D icon, string label, int maxIconWidth = 0) : base(windowManager)\n        {\n            this.icon = icon;\n            this.label = label;\n            this.maxIconWidth = maxIconWidth;\n\n            DrawBorders = false;\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            base.Draw(gameTime);\n\n            if (icon == null)\n                return;\n\n            var textSize = Renderer.GetTextDimensions(label, FontIndex);\n            int textHeight = (int)textSize.Y;\n\n            int iconY = (textHeight - icon.Height) / 2;\n            if (iconY < 0) \n                iconY = 0;\n\n            DrawTexture(icon, new Rectangle(0, iconY, icon.Width, icon.Height), Color.White);\n\n            int panelHeight = Math.Max(icon.Height, textHeight);\n            float textY = (panelHeight - textHeight) / 2f;\n\n            int textStartX = maxIconWidth > 0 ? maxIconWidth + iconLabelSpacing : icon.Width + iconLabelSpacing;\n\n            DrawString(label, FontIndex, new Vector2(textStartX, textY), UISettings.ActiveSettings.TextColor);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing Image = SixLabors.ImageSharp.Image;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// A UI panel that displays information about a hosted CnCNet or LAN game.\n    /// </summary>\n    public class GameInformationPanel : XNAPanel\n    {\n        private const int MAX_PLAYERS = 8;\n\n        public GameInformationPanel(WindowManager windowManager, MapLoader mapLoader, GameLobbyBase gameLobby = null)\n            : base(windowManager)\n        {\n            this.mapLoader = mapLoader;\n            this.gameLobby = gameLobby;\n            DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET;\n        }\n\n        private MapLoader mapLoader;\n        private GameLobbyBase gameLobby;\n\n        private XNALabel lblGameInformation;\n        private XNALabel lblGameMode;\n        private XNALabel lblMap;\n        private XNALabel lblGameVersion;\n        private XNALabel lblHost;\n        private XNALabel lblPing;\n        private XNALabel lblPlayers;\n        private XNALabel lblSkillLevel;\n\n        private XNALabel[] lblPlayerNames;\n        private XNAPanel pnlIconLegend;\n        private XNAPanel pnlGameOptions;\n\n        private GenericHostedGame game = null;\n\n        /// <summary>\n        /// 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`.\n        /// </summary>\n        private bool mapPreviewTextureNeedsDispose = false;\n        private Texture2D mapPreviewTexture = null;\n\n        private Texture2D noMapPreviewTexture = null;\n\n        private Texture2D txLockedGame;\n        private Texture2D txIncompatibleGame;\n        private Texture2D txPasswordedGame;\n\n        private const int leftColumnPositionX = 10;\n        private int rightColumnPositionX = 0;\n        private int mapPreviewPositionY = 0;\n        private const int columnMargin = 10;\n        private const int topStartingPositionY = 30;\n        private const int rowHeight = 24;\n        private const int initialPanelHeight = 260;\n        private const int columnWidth = 235;\n        private const int maxPreviewHeight = 150;\n        private const int mapPreviewMargin = 15;\n\n        private const int playerNameRowHeight = 20;\n        private const int playerColumn2OffsetX = 115;\n\n        private const int legendTopSpacing = 15;\n        private const int legendIconHeight = 18;\n        private const int legendPadding = 5;\n        private const int legendIconPadding = 2;\n\n        private const int gameInfoLabelTopPadding = 6;\n\n        private const int mapPreviewHorizontalMargin = 10;\n        private const int mapPreviewVerticalMargin = 20;\n\n        private string[] skillLevelOptions;\n\n        public override void Initialize()\n        {\n            ClientRectangle = new Rectangle(0, 0, columnWidth * 2, initialPanelHeight);\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            lblGameInformation = new XNALabel(WindowManager);\n            lblGameInformation.FontIndex = 1;\n            lblGameInformation.Text = \"GAME INFORMATION\".L10N(\"Client:Main:GameInfo\");\n\n            if (AssetLoader.AssetExists(\"noMapPreview.png\"))\n                noMapPreviewTexture = AssetLoader.LoadTexture(\"noMapPreview.png\");\n\n            txLockedGame = AssetLoader.LoadTexture(\"lockedgame.png\");\n            txIncompatibleGame = AssetLoader.LoadTexture(\"incompatible.png\");\n            txPasswordedGame = AssetLoader.LoadTexture(\"passwordedgame.png\");\n\n            rightColumnPositionX = Width / 2 - columnMargin;\n            mapPreviewPositionY = topStartingPositionY + (rowHeight * 2 + mapPreviewMargin); // 2 Labels down, incase map name spills to next line\n\n            // Right Column\n            // Includes Game mode, Map name, and the Map preview (See RenderMapPreview for that)\n            lblGameMode = new XNALabel(WindowManager);\n            lblGameMode.ClientRectangle = new Rectangle(rightColumnPositionX, topStartingPositionY, 0, 0);\n\n            lblMap = new XNALabel(WindowManager);\n            lblMap.ClientRectangle = new Rectangle(rightColumnPositionX, topStartingPositionY + rowHeight, 0, 0);\n\n\n            // Left Column\n            // Includes Host, Ping, Version, and Players\n            lblHost = new XNALabel(WindowManager);\n            lblHost.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY, 0, 0);\n\n            lblPing = new XNALabel(WindowManager);\n            lblPing.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + rowHeight, 0, 0);\n\n            lblGameVersion = new XNALabel(WindowManager);\n            lblGameVersion.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 2), 0, 0);\n\n            lblSkillLevel = new XNALabel(WindowManager);\n            lblSkillLevel.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 3), 0, 0);\n\n            lblPlayers = new XNALabel(WindowManager);\n            lblPlayers.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 4), 0, 0);\n\n            lblPlayerNames = new XNALabel[MAX_PLAYERS];\n            for (int i = 0; i < lblPlayerNames.Length / 2; i++)\n            {\n                XNALabel lblPlayerName1 = new XNALabel(WindowManager);\n                lblPlayerName1.ClientRectangle = new Rectangle(lblPlayers.X, lblPlayers.Y + rowHeight + i * playerNameRowHeight, 0, 0);\n                lblPlayerName1.RemapColor = UISettings.ActiveSettings.AltColor;\n\n                XNALabel lblPlayerName2 = new XNALabel(WindowManager);\n                lblPlayerName2.ClientRectangle = new Rectangle(lblPlayers.X + playerColumn2OffsetX, lblPlayerName1.Y, 0, 0);\n                lblPlayerName2.RemapColor = UISettings.ActiveSettings.AltColor;\n\n                AddChild(lblPlayerName1);\n                AddChild(lblPlayerName2);\n\n                lblPlayerNames[i] = lblPlayerName1;\n                lblPlayerNames[(lblPlayerNames.Length / 2) + i] = lblPlayerName2;\n            }\n\n            pnlIconLegend = new XNAPanel(WindowManager);\n            int legendY = lblPlayers.Y + rowHeight + (MAX_PLAYERS / 2 * playerNameRowHeight) + legendTopSpacing;\n            pnlIconLegend.ClientRectangle = new Rectangle(0, legendY, columnWidth, 0);\n            pnlIconLegend.DrawBorders = false;\n\n            pnlGameOptions = new XNAPanel(WindowManager);\n            pnlGameOptions.ClientRectangle = new Rectangle(0, legendY, columnWidth * 2, 0);\n            pnlGameOptions.DrawBorders = false;\n\n            AddChild(lblGameMode);\n            AddChild(lblMap);\n            AddChild(lblGameVersion);\n            AddChild(lblHost);\n            AddChild(lblPing);\n            AddChild(lblPlayers);\n            AddChild(lblGameInformation);\n            AddChild(lblSkillLevel);\n            AddChild(pnlGameOptions);\n            AddChild(pnlIconLegend);\n\n            lblGameInformation.CenterOnParent();\n            lblGameInformation.ClientRectangle = new Rectangle(lblGameInformation.X, gameInfoLabelTopPadding,\n                lblGameInformation.Width, lblGameInformation.Height);\n\n            skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n\n            base.Initialize();\n        }\n\n        public void SetInfo(GenericHostedGame game)\n        {\n            ClearInfo();\n\n            this.game = game;\n\n            string translatedMapName = \"Unknown\".L10N(\"Client:Main:Unknown\");\n\n            if (!string.IsNullOrEmpty(game.MapHash) && mapLoader != null)\n            {\n                Map map = mapLoader.FindMapByHash(game.MapHash);\n\n                if (map != null)\n                    translatedMapName = map.Name ?? map.UntranslatedName;\n                else if (!string.IsNullOrEmpty(game.Map))\n                    translatedMapName = game.Map; // fallback to broadcasted name\n            }\n            else if (!string.IsNullOrEmpty(game.Map))\n            {\n                translatedMapName = game.Map;\n            }\n\n            string translatedGameModeName = string.IsNullOrEmpty(game.GameMode)\n                ? \"Unknown\".L10N(\"Client:Main:Unknown\") : game.GameMode.L10N($\"INI:GameModes:{game.GameMode}:UIName\", notify: false);\n\n            lblGameMode.Text = Renderer.GetStringWithLimitedWidth(\"Game mode:\".L10N(\"Client:Main:GameInfoGameMode\") + \" \" + Renderer.GetSafeString(translatedGameModeName, lblGameMode.FontIndex),\n               lblGameMode.FontIndex, Width - lblGameMode.X);\n            lblGameMode.Visible = true;\n\n            lblMap.Text = Renderer.GetStringWithLimitedWidth(\"Map:\".L10N(\"Client:Main:GameInfoMap\") + \" \" + Renderer.GetSafeString(translatedMapName, lblMap.FontIndex),\n                            lblMap.FontIndex, Width - lblMap.X);\n            lblMap.Visible = true;\n\n            lblMap.Text = Renderer.FixText(lblMap.Text, lblMap.FontIndex, columnWidth).Text;\n            lblMap.Visible = true;\n\n            lblGameVersion.Text = \"Game version:\".L10N(\"Client:Main:GameInfoGameVersion\") + \" \" + Renderer.GetSafeString(game.GameVersion, lblGameVersion.FontIndex);\n            lblGameVersion.Visible = true;\n\n            lblHost.Text = \"Host:\".L10N(\"Client:Main:GameInfoHost\") + \" \" + Renderer.GetSafeString(game.HostName, lblHost.FontIndex);\n            lblHost.Visible = true;\n\n            lblPing.Text = game.Ping > 0 ? \"Ping:\".L10N(\"Client:Main:GameInfoPing\") + \" \" + game.Ping.ToString() + \" ms\" : \"Ping: Unknown\".L10N(\"Client:Main:GameInfoPingUnknown\");\n            lblPing.Visible = true;\n\n            lblPlayers.Visible = true;\n            lblPlayers.Text = \"Players\".L10N(\"Client:Main:GameInfoPlayers\") + \" (\" + game.Players.Length + \" / \" + game.MaxPlayers + \"):\";\n\n            for (int i = 0; i < game.Players.Length && i < MAX_PLAYERS; i++)\n            {\n                lblPlayerNames[i].Visible = true;\n                lblPlayerNames[i].Text = Renderer.GetSafeString(game.Players[i], lblPlayerNames[i].FontIndex);\n            }\n\n            for (int i = game.Players.Length; i < MAX_PLAYERS; i++)\n            {\n                lblPlayerNames[i].Visible = false;\n            }\n\n            string skillLevel = skillLevelOptions[game.SkillLevel];\n            string localizedSkillLevel = skillLevel.L10N($\"INI:ClientDefinitions:SkillLevel:{game.SkillLevel}\");\n            lblSkillLevel.Text = \"Preferred Skill Level:\".L10N(\"Client:Main:GameInfoSkillLevel\") + \" \" + localizedSkillLevel;\n            lblSkillLevel.Visible = true;\n\n            lblGameInformation.Visible = true;\n\n            if (mapLoader != null && !string.IsNullOrEmpty(game.MapHash))\n            {\n                Debug.Assert(!mapPreviewTextureNeedsDispose, \"previous texture must be disposed before loading a new texture\");\n\n                Map map = mapLoader.FindMapByHash(game.MapHash);\n\n                Image mapPreviewImage = map != null ? mapLoader.GetCachedPreviewImageFromMap(map, syncLoadOnCacheMiss: false) : null;\n\n                if (mapPreviewImage != null)\n                {\n                    mapPreviewTexture = AssetLoader.TextureFromImage(mapPreviewImage);\n                    mapPreviewTextureNeedsDispose = true;\n                }\n                else if (noMapPreviewTexture != null)\n                {\n                    Debug.Assert(!noMapPreviewTexture.IsDisposed, \"noMapPreviewTexture should never be disposed.\");\n                    mapPreviewTexture = noMapPreviewTexture;\n                    mapPreviewTextureNeedsDispose = false;\n                }\n                else\n                {\n                    mapPreviewTexture = null;\n                    mapPreviewTextureNeedsDispose = false;\n                }\n            }\n            else\n            {\n                if (mapPreviewTextureNeedsDispose &&\n                    mapPreviewTexture != null &&\n                    !mapPreviewTexture.IsDisposed)\n                {\n                    mapPreviewTexture.Dispose();\n                }\n\n                mapPreviewTexture = null;\n                mapPreviewTextureNeedsDispose = false;\n            }\n            SetGameOptionsInfo(game);\n            SetLegendInfo(game);\n        }\n\n        private void SetGameOptionsInfo(GenericHostedGame game)\n        {\n            foreach (XNAControl xnaControl in pnlGameOptions.Children.ToList())\n                pnlGameOptions.RemoveChild(xnaControl);\n\n            if (gameLobby == null || !(game is HostedCnCNetGame cncnetGame) ||\n                cncnetGame.BroadcastedGameOptionValues == null)\n            {\n                pnlGameOptions.Visible = false;\n                return;\n            }\n\n            var broadcastableSettings = gameLobby.GetBroadcastableSettings();\n            var optionIconsWithText = new List<(Texture2D icon, string text, int sortOrder)>();\n            var optionIconsOnly = new List<(Texture2D icon, int sortOrder)>();\n\n            for (int i = 0; i < broadcastableSettings.Count && i < cncnetGame.BroadcastedGameOptionValues.Length; i++)\n            {\n                var setting = broadcastableSettings[i];\n                int value = cncnetGame.BroadcastedGameOptionValues[i];\n\n                if (setting is GameLobbyCheckBox checkbox && checkbox.ShowInGameInformationPanel)\n                {\n                    bool isChecked = value != 0;\n                    string iconName = isChecked ? checkbox.EnabledIcon : checkbox.DisabledIcon;\n                    if (!string.IsNullOrEmpty(iconName))\n                    {\n                        Texture2D icon = AssetLoader.LoadTexture(iconName);\n                        if (icon != null)\n                        {\n                            if (checkbox.ShowInGameInformationPanelAsIconOnly)\n                            {\n                                // Show icon only\n                                optionIconsOnly.Add((icon, checkbox.SortOrder));\n                            }\n                            else\n                            {\n                                // Show with text\n                                string text = $\"{checkbox.Text}: {(isChecked ? \"On\".L10N(\"Client:Main:On\") : \"Off\".L10N(\"Client:Main:Off\"))}\";\n                                optionIconsWithText.Add((icon, text, checkbox.SortOrder));\n                            }\n                        }\n                    }\n                }\n                else if (setting is GameLobbyDropDown dropdown && dropdown.ShowInGameInformationPanel)\n                {\n                    if (value >= 0 && value < dropdown.Items.Count)\n                    {\n                        Texture2D icon = dropdown.Items[value].Texture;\n                        if (icon != null)\n                        {\n                            if (dropdown.ShowInGameInformationPanelAsIconOnly)\n                            {\n                                // Show icon only\n                                optionIconsOnly.Add((icon, dropdown.SortOrder));\n                            }\n                            else\n                            {\n                                // Show with text\n                                string text = $\"{dropdown.OptionName}: {dropdown.Items[value].Text}\";\n                                optionIconsWithText.Add((icon, text, dropdown.SortOrder));\n                            }\n                        }\n                    }\n                }\n            }\n\n            if (optionIconsWithText.Count == 0 && optionIconsOnly.Count == 0)\n            {\n                pnlGameOptions.Visible = false;\n                return;\n            }\n\n            int gameOptionsY = lblPlayers.Y + rowHeight + (MAX_PLAYERS / 2 * playerNameRowHeight) + legendTopSpacing;\n            pnlGameOptions.ClientRectangle = new Rectangle(0, gameOptionsY, columnWidth * 2, 0);\n\n            var divider = CreateDivider(0);\n            pnlGameOptions.AddChild(divider);\n\n            int currentY = divider.Bottom + legendPadding;\n\n            // First show icons in a row\n            if (optionIconsOnly.Count > 0)\n            {\n                var sortedIconsOnly = optionIconsOnly.OrderBy(x => x.sortOrder).ToList();\n                int iconX = leftColumnPositionX;\n                const int iconSpacing = 4;\n                int maxIconHeight = sortedIconsOnly.Max(x => x.icon.Height);\n\n                foreach (var (icon, _) in sortedIconsOnly)\n                {\n                    var iconPanel = new GameInformationIconOnlyPanel(WindowManager, icon);\n                    iconPanel.ClientRectangle = new Rectangle(iconX, currentY, icon.Width, icon.Height);\n                    pnlGameOptions.AddChild(iconPanel);\n                    iconX += icon.Width + iconSpacing;\n                }\n\n                currentY += maxIconHeight + legendPadding;\n            }\n\n            // Then show icons with text (two columns)\n            if (optionIconsWithText.Count > 0)\n            {\n                var sortedIconsWithText = optionIconsWithText.OrderBy(x => x.sortOrder).ToList();\n                int maxIconWidth = sortedIconsWithText.Max(option => option.icon.Width);\n\n                int itemIndex = 0;\n                int leftY = currentY;\n                int rightY = currentY;\n\n                foreach (var (icon, label, _) in sortedIconsWithText)\n                {\n                    bool isLeftColumn = (itemIndex % 2 == 0);\n                    int xPosition = isLeftColumn ? leftColumnPositionX : rightColumnPositionX;\n                    int yPosition = isLeftColumn ? leftY : rightY;\n\n                    var iconPanel = new GameInformationIconPanel(WindowManager, icon, label, maxIconWidth);\n                    iconPanel.ClientRectangle = new Rectangle(xPosition, yPosition, columnWidth, legendIconHeight);\n                    pnlGameOptions.AddChild(iconPanel);\n\n                    if (isLeftColumn)\n                        leftY += legendIconHeight + legendIconPadding;\n                    else\n                        rightY += legendIconHeight + legendIconPadding;\n\n                    itemIndex++;\n                }\n\n                currentY = Math.Max(leftY, rightY);\n            }\n\n            pnlGameOptions.Height = currentY + legendPadding;\n            pnlGameOptions.Visible = true;\n\n            pnlIconLegend.ClientRectangle = new Rectangle(pnlIconLegend.X, pnlGameOptions.Bottom, pnlIconLegend.Width, pnlIconLegend.Height);\n        }\n\n        private void SetLegendInfo(GenericHostedGame game)\n        {\n            ClearLegendIconPanel();\n\n            var icons = new List<(Texture2D, string)>();\n            if (game.Locked) icons.Add((txLockedGame, \"Game is locked\".L10N(\"Client:Main:LockedGame\")));\n            if (game.Passworded) icons.Add((txPasswordedGame, \"Game is passworded\".L10N(\"Client:Main:PasswordedGame\")));\n            if (game.Incompatible) icons.Add((txIncompatibleGame, \"Incompatible client version\".L10N(\"Client:Main:IncompatibleGame\")));\n\n            if (icons.Count == 0)\n            {\n                pnlIconLegend.Visible = false;\n                UpdatePanelHeight();\n                return;\n            }\n\n            var divider = CreateDivider(0);\n            pnlIconLegend.AddChild(divider);\n\n            int currentY = divider.Bottom + legendPadding;\n\n            foreach (var (icon, label) in icons)\n            {\n                var iconPanel = new GameInformationIconPanel(WindowManager, icon, label);\n                iconPanel.ClientRectangle = new Rectangle(leftColumnPositionX, currentY, pnlIconLegend.Width, legendIconHeight);\n                pnlIconLegend.AddChild(iconPanel);\n                currentY += legendIconHeight;\n            }\n\n            pnlIconLegend.Height = currentY + legendPadding;\n            pnlIconLegend.Visible = true;\n\n            UpdatePanelHeight();\n        }\n\n        private void UpdatePanelHeight()\n        {\n            int bottomMostY = initialPanelHeight;\n\n            if (pnlGameOptions.Visible && pnlGameOptions.Bottom > bottomMostY)\n                bottomMostY = pnlGameOptions.Bottom;\n\n            if (pnlIconLegend.Visible && pnlIconLegend.Bottom > bottomMostY)\n                bottomMostY = pnlIconLegend.Bottom;\n\n            ClientRectangle = new Rectangle(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, bottomMostY);\n        }\n\n        private XNAPanel CreateDivider(int y, int height = 1)\n        {\n            var dividerPanel = new XNAPanel(WindowManager);\n            dividerPanel.DrawBorders = true;\n            dividerPanel.ClientRectangle = new Rectangle(0, y, ClientRectangle.Width, height);\n            return dividerPanel;\n        }\n\n        private void ClearLegendIconPanel()\n        {\n            foreach (XNAControl xnaControl in pnlIconLegend.Children.ToList())\n                pnlIconLegend.RemoveChild(xnaControl);\n        }\n\n        public void ClearInfo()\n        {\n            lblGameMode.Visible = false;\n            lblMap.Visible = false;\n            lblGameVersion.Visible = false;\n            lblHost.Visible = false;\n            lblPing.Visible = false;\n            lblPlayers.Visible = false;\n            lblGameInformation.Visible = false;\n            lblSkillLevel.Visible = false;\n\n            foreach (XNALabel label in lblPlayerNames)\n                label.Visible = false;\n\n            if (mapPreviewTexture != null && mapPreviewTextureNeedsDispose)\n            {\n                Debug.Assert(!mapPreviewTexture.IsDisposed, \"mapPreviewTexture should not be disposed before this call\");\n                mapPreviewTexture.Dispose();\n                mapPreviewTexture = null;\n                mapPreviewTextureNeedsDispose = false;\n            }\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            if (Alpha > 0.0f)\n            {\n                base.Draw(gameTime);\n\n                if (game != null && mapPreviewTexture != null)\n                    RenderMapPreview();\n            }\n        }\n\n        private void RenderMapPreview()\n        {\n            // Calculate map preview area based on right half of ClientRectangle\n            double xRatio = (ClientRectangle.Width / 2 - mapPreviewHorizontalMargin) / (double)mapPreviewTexture.Width;\n            double yRatio = (ClientRectangle.Height - mapPreviewVerticalMargin) / (double)mapPreviewTexture.Height;\n\n            double ratio = Math.Min(xRatio, yRatio); // Choose the smaller ratio for scaling\n            int textureWidth = (int)(mapPreviewTexture.Width * ratio);\n            int textureHeight = (int)(mapPreviewTexture.Height * ratio);\n\n            // Apply max height constraint\n            if (textureHeight > maxPreviewHeight)\n            {\n                ratio = maxPreviewHeight / (double)mapPreviewTexture.Height;\n                textureHeight = maxPreviewHeight;\n                textureWidth = (int)(mapPreviewTexture.Width * ratio); // Recalculate width to maintain aspect ratio\n            }\n\n            int texturePositionX = rightColumnPositionX + (ClientRectangle.Width / 2 - textureWidth) / 2; // Center in the right column\n            int texturePositionY = mapPreviewPositionY;\n\n            DrawTexture(\n                mapPreviewTexture,\n                new Rectangle(texturePositionX, texturePositionY, textureWidth, textureHeight),\n                Color.White\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameListBox.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\n\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// A list box for listing hosted games.\n    /// </summary>\n    public class GameListBox : XNAListBox\n    {\n        private const int GAME_REFRESH_RATE = 1;\n        private const int ICON_MARGIN = 2;\n        private static string LOADED_GAME_TEXT => \" (\" + \"Loaded Game\".L10N(\"Client:Main:LoadedGame\") + \")\";\n        private readonly string[] SkillLevelOptions;\n\n        public GameListBox(WindowManager windowManager, MapLoader mapLoader,\n            string localGameIdentifier, GameLobbyBase gameLobby = null,\n            Predicate<GenericHostedGame> gameMatchesFilter = null)\n            : base(windowManager)\n        {\n            this.mapLoader = mapLoader;\n            this.localGameIdentifier = localGameIdentifier;\n            this.gameLobby = gameLobby;\n            GameMatchesFilter = gameMatchesFilter;\n\n            SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n        }\n\n        private List<Texture2D?> txSkillLevelIcons = new();\n\n        private int loadedGameTextWidth;\n\n        public List<GenericHostedGame> HostedGames = new();\n\n        public double GameLifetime { get; set; } = 35.0;\n\n        /// <summary>\n        /// A predicate for setting a filter expression for displayed games.\n        /// </summary>\n        private Predicate<GenericHostedGame> GameMatchesFilter { get; }\n\n        private Texture2D txLockedGame;\n        private Texture2D txIncompatibleGame;\n        private Texture2D txPasswordedGame;\n\n        private string localGameIdentifier;\n\n        private MapLoader mapLoader;\n\n        private GameLobbyBase gameLobby;\n\n        private GameInformationPanel panelGameInformation;\n\n        private TimeSpan timeSinceGameRefresh;\n\n        private Color hoverOnGameColor;\n\n        /// <summary>\n        /// Removes a game from the list.\n        /// </summary>\n        /// <param name=\"index\">The index of the game to remove.</param>\n        public void RemoveGame(int index)\n        {\n            HostedGames.RemoveAt(index);\n\n            Refresh();\n        }\n\n        /// <summary>\n        /// Compares each listed XNAListBoxItem item in the GameListBox to the refernece XNAListBoxItem item for equality.\n        /// </summary>\n        /// <param name=\"referencedItem\">The XNAListBoxItem to compare against</param>\n        /// <returns>bool</returns>\n        private static Predicate<XNAListBoxItem> GameListMatch(XNAListBoxItem referencedItem) => listedItem =>\n        {\n            var referencedGame = (GenericHostedGame)referencedItem?.Tag;\n            var listedGame = (GenericHostedGame)listedItem?.Tag;\n\n            if (referencedGame == null || listedGame == null)\n                return false;\n\n            return referencedGame.Equals(listedGame);\n        };\n\n        /// <summary>\n        /// Refreshes game information in the game list box.\n        /// </summary>\n        public void Refresh()\n        {\n            var selectedItem = SelectedItem;\n            var hoveredItem = HoveredItem;\n\n            Clear();\n\n            GetSortedAndFilteredGames()\n                .ToList()\n                .ForEach(AddGameToList);\n\n            if (selectedItem != null)\n                SelectedIndex = Items.FindIndex(GameListMatch(selectedItem));\n            if (hoveredItem != null)\n                HoveredIndex = Items.FindIndex(GameListMatch(hoveredItem));\n\n            ShowGamePanelInfoForIndex(IsValidGameIndex(SelectedIndex) ? SelectedIndex : HoveredIndex);\n        }\n\n        /// <summary>\n        /// Adds a game to the game list.\n        /// </summary>\n        /// <param name=\"game\">The game to add.</param>\n        public void AddGame(GenericHostedGame game)\n        {\n            HostedGames.Add(game);\n\n            // Early notify the map preview cache\n            mapLoader.PrefetchCachedPreviewImageFromMap(mapLoader.FindMapByHash(game.MapHash));\n\n            Refresh();\n        }\n\n        private IEnumerable<GenericHostedGame> GetSortedAndFilteredGames()\n        {\n            var sortedGames = GetSortedGames();\n\n            return GameMatchesFilter == null ? sortedGames : sortedGames.Where(hg => GameMatchesFilter(hg));\n        }\n\n        private IEnumerable<GenericHostedGame> GetSortedGames()\n        {\n            var sortedGames =\n                HostedGames\n                    .OrderBy(hg => hg.Locked)\n                    .ThenBy(hg => string.Equals(hg.Game.InternalName, localGameIdentifier, StringComparison.InvariantCultureIgnoreCase))\n                    .ThenBy(hg => hg.GameVersion != ProgramConstants.GAME_VERSION)\n                    .ThenBy(hg => hg.Passworded);\n\n            switch ((SortDirection)UserINISettings.Instance.SortState.Value)\n            {\n                case SortDirection.Asc:\n                    sortedGames = sortedGames.ThenBy(hg => hg.RoomName);\n                    break;\n                case SortDirection.Desc:\n                    sortedGames = sortedGames.ThenByDescending(hg => hg.RoomName);\n                    break;\n            }\n\n            return sortedGames;\n        }\n\n        /// <summary>\n        /// Sorts and refreshes the game information in the game list box.\n        /// </summary>\n        public void SortAndRefreshHostedGames()\n        {\n            Refresh();\n        }\n\n        public void ClearGames()\n        {\n            Clear();\n            HostedGames.Clear();\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            txLockedGame = AssetLoader.LoadTexture(\"lockedgame.png\");\n            txIncompatibleGame = AssetLoader.LoadTexture(\"incompatible.png\");\n            txPasswordedGame = AssetLoader.LoadTexture(\"passwordedgame.png\");\n\n            panelGameInformation = new GameInformationPanel(WindowManager, mapLoader, gameLobby);\n            panelGameInformation.Name = nameof(panelGameInformation);\n            panelGameInformation.BackgroundTexture = AssetLoader.LoadTexture(\"cncnetlobbypanelbg.png\");\n            panelGameInformation.DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET;\n            panelGameInformation.Initialize();\n            panelGameInformation.ClearInfo();\n            panelGameInformation.Disable();\n            panelGameInformation.InputEnabled = false;\n            panelGameInformation.Alpha = 0f;\n            Parent.AddChild(panelGameInformation); // make this a child of our parent so it's not drawn on our rendertarget\n\n            SelectedIndexChanged += GameListBox_SelectedIndexChanged;\n            HoveredIndexChanged += GameListBox_HoveredIndexChanged;\n\n            hoverOnGameColor = AssetLoader.GetColorFromString(\n                ClientConfiguration.Instance.HoverOnGameColor);\n\n            loadedGameTextWidth = (int)Renderer.GetTextDimensions(LOADED_GAME_TEXT, FontIndex).X;\n\n            InitSkillLevelIcons();\n        }\n\n        private void InitSkillLevelIcons()\n        {\n            for (int i = 0; i < SkillLevelOptions.Length; i++)\n            {\n                string fileName = $\"skillLevel{i}.png\";\n\n                txSkillLevelIcons.Add(AssetLoader.AssetExists(fileName)\n                    ? AssetLoader.LoadTexture(fileName)\n                    : null);\n            }\n        }\n\n        private bool IsValidGameIndex(int index)\n        {\n            return index >= 0 && index < Items.Count;\n        }\n\n        private void ShowGamePanelInfoForIndex(int index)\n        {\n            if (!IsValidGameIndex(index))\n            {\n                panelGameInformation.AlphaRate = -0.5f;\n                return;\n            }\n\n            panelGameInformation.Enable();\n            panelGameInformation.X = Right;\n            panelGameInformation.Y = Y;\n\n            panelGameInformation.AlphaRate = 0.5f;\n\n            var hostedGame = (GenericHostedGame)Items[index].Tag;\n            panelGameInformation.SetInfo(hostedGame);\n        }\n\n        private void GameListBox_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            ShowGamePanelInfoForIndex(SelectedIndex);\n        }\n\n        private void GameListBox_HoveredIndexChanged(object sender, EventArgs e)\n        {\n            if (!IsValidGameIndex(SelectedIndex))\n                ShowGamePanelInfoForIndex(HoveredIndex);\n        }\n\n        private (List<Texture2D> leftIcons, List<Texture2D> rightIcons) GetGameOptionIcons(GenericHostedGame game)\n        {\n            var leftIcons = new List<Texture2D>();\n            var rightIcons = new List<Texture2D>();\n\n            if (gameLobby == null || game is not HostedCnCNetGame cncnetGame)\n                return (leftIcons, rightIcons);\n\n            if (cncnetGame.BroadcastedGameOptionValues == null || cncnetGame.BroadcastedGameOptionValues.Length == 0)\n                return (leftIcons, rightIcons);\n\n            var broadcastableSettings = gameLobby.GetBroadcastableSettings();\n\n            for (int i = 0; i < broadcastableSettings.Count && i < cncnetGame.BroadcastedGameOptionValues.Length; i++)\n            {\n                var setting = broadcastableSettings[i];\n                int value = cncnetGame.BroadcastedGameOptionValues[i];\n\n                if (setting is GameLobbyCheckBox checkbox && checkbox.ShowInGameList)\n                {\n                    string iconName = value != 0 ? checkbox.EnabledIcon : checkbox.DisabledIcon;\n                    if (string.IsNullOrEmpty(iconName))\n                        continue;\n\n                    Texture2D icon = AssetLoader.LoadTexture(iconName);\n                    if (icon != null)\n                    {\n                        if (checkbox.ShowInGameListOnRight)\n                            rightIcons.Add(icon);\n                        else\n                            leftIcons.Add(icon);\n                    }\n                }\n                else if (setting is GameLobbyDropDown dropdown && dropdown.ShowInGameList)\n                {\n                    // Use the icon for the selected value\n                    if (value >= 0 && value < dropdown.Items.Count)\n                    {\n                        Texture2D icon = dropdown.Items[value].Texture;\n                        if (icon != null)\n                        {\n                            if (dropdown.ShowInGameListOnRight)\n                                rightIcons.Add(icon);\n                            else\n                                leftIcons.Add(icon);\n                        }\n                    }\n                }\n            }\n\n            return (leftIcons, rightIcons);\n        }\n\n        private void AddGameToList(GenericHostedGame hg)\n        {\n            int lgTextWidth = hg.IsLoadedGame ? loadedGameTextWidth : 0;\n\n            var (leftIcons, rightIcons) = GetGameOptionIcons(hg);\n            int leftIconsWidth = leftIcons.Count > 0 ?\n                (leftIcons.Sum(icon => icon.Width) + (leftIcons.Count * ICON_MARGIN)) : 0;\n            int rightIconsWidth = rightIcons.Count > 0 ?\n                (rightIcons.Sum(icon => icon.Width) + (rightIcons.Count * ICON_MARGIN)) : 0;\n\n            bool showGameIcon = ClientConfiguration.Instance.ShowGameIconInGameList\n                || hg.Game.InternalName != localGameIdentifier.ToLower();\n            int gameTextureWidth = showGameIcon ? hg.Game.Texture.Width : 0;\n\n            int skillLevelIconWidth = 0;\n            if (txSkillLevelIcons[hg.SkillLevel] != null)\n                skillLevelIconWidth = txSkillLevelIcons[hg.SkillLevel].Width;\n\n            int maxTextWidth = Width - gameTextureWidth -\n                (hg.Incompatible ? txIncompatibleGame.Width : 0) -\n                (hg.Locked ? txLockedGame.Width : 0) -\n                (hg.Passworded ? txPasswordedGame.Width : 0) -\n                skillLevelIconWidth -\n                leftIconsWidth - rightIconsWidth -\n                (ICON_MARGIN * 3) - GetScrollBarWidth() - lgTextWidth;\n\n            var lbItem = new XNAListBoxItem();\n            lbItem.Tag = hg;\n            lbItem.Text = Renderer.GetStringWithLimitedWidth(Renderer.GetSafeString(\n                hg.RoomName, FontIndex), FontIndex, maxTextWidth);\n\n            if (hg.Game.InternalName != localGameIdentifier.ToLower())\n                lbItem.TextColor = UISettings.ActiveSettings.TextColor;\n            //else // made unnecessary by new Rampastring.XNAUI\n            //    lbItem.TextColor = UISettings.ActiveSettings.AltColor;\n\n            if (hg.Incompatible || hg.Locked)\n            {\n                lbItem.TextColor = Color.Gray;\n            }\n\n            AddItem(lbItem);\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            timeSinceGameRefresh += gameTime.ElapsedGameTime;\n\n            if (timeSinceGameRefresh.TotalSeconds > GAME_REFRESH_RATE)\n            {\n                for (int i = 0; i < HostedGames.Count; i++)\n                {\n                    if (DateTime.Now - HostedGames[i].LastRefreshTime > TimeSpan.FromSeconds(GameLifetime))\n                    {\n                        HostedGames.RemoveAt(i);\n                        i--;\n                    }\n                }\n\n                Refresh();\n\n                timeSinceGameRefresh = TimeSpan.Zero;\n            }\n\n            base.Update(gameTime);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            DrawPanel();\n\n            int height = 2;\n\n            for (int i = TopIndex; i < Items.Count; i++)\n            {\n                var lbItem = Items[i];\n\n                if (height + lbItem.TextLines.Count * LineHeight > Height)\n                    break;\n\n                int x = TextBorderDistance;\n\n                bool scrollBarDrawn = ScrollBar.IsDrawn() && EnableScrollbar;\n                int drawnWidth = !scrollBarDrawn || DrawSelectionUnderScrollbar ? Width - 2 : Width - 2 - ScrollBar.Width;\n\n                if (i == SelectedIndex)\n                {\n                    FillRectangle(\n                        new Rectangle(1, height, drawnWidth, lbItem.TextLines.Count * LineHeight),\n                        FocusColor);\n                }\n                else if (i == HoveredIndex)\n                {\n                    FillRectangle(\n                        new Rectangle(1, height, drawnWidth, lbItem.TextLines.Count * LineHeight),\n                        hoverOnGameColor);\n                }\n\n                var hostedGame = (GenericHostedGame)lbItem.Tag;\n\n                // left-side game option icons\n                var (leftIcons, rightIcons) = GetGameOptionIcons(hostedGame);\n                foreach (var icon in leftIcons)\n                {\n                    DrawTexture(icon,\n                        new Rectangle(x, height,\n                        icon.Width, icon.Height), Color.White);\n                    x += icon.Width + ICON_MARGIN;\n                }\n\n                bool showGameIcon = ClientConfiguration.Instance.ShowGameIconInGameList\n                    || hostedGame.Game.InternalName != localGameIdentifier.ToLower();\n\n                if (showGameIcon)\n                {\n                    DrawTexture(hostedGame.Game.Texture,\n                        new Rectangle(x, height,\n                        hostedGame.Game.Texture.Width, hostedGame.Game.Texture.Height), Color.White);\n\n                    x += hostedGame.Game.Texture.Width + ICON_MARGIN;\n                }\n\n                if (hostedGame.Locked)\n                {\n                    DrawTexture(txLockedGame,\n                        new Rectangle(x, height,\n                        txLockedGame.Width, txLockedGame.Height), Color.White);\n                    x += txLockedGame.Width + ICON_MARGIN;\n                }\n\n                if (hostedGame.Incompatible)\n                {\n                    DrawTexture(txIncompatibleGame,\n                        new Rectangle(x, height,\n                        txIncompatibleGame.Width, txIncompatibleGame.Height), Color.White);\n                    x += txIncompatibleGame.Width + ICON_MARGIN;\n                }\n\n                // right-side icons (right game option icons, then password, then skill level)\n                int rightX = Width - TextBorderDistance - (scrollBarDrawn ? ScrollBar.Width : 0);\n\n                // right-side game option icons (drawn first, from right to left)\n                for (int iconIndex = rightIcons.Count - 1; iconIndex >= 0; iconIndex--)\n                {\n                    var icon = rightIcons[iconIndex];\n                    rightX -= icon.Width;\n                    DrawTexture(icon,\n                        new Rectangle(rightX, height, icon.Width, icon.Height), Color.White);\n                    rightX -= ICON_MARGIN;\n                }\n\n                // password icon\n                if (hostedGame.Passworded)\n                {\n                    rightX -= txPasswordedGame.Width;\n                    DrawTexture(txPasswordedGame,\n                        new Rectangle(rightX, height, txPasswordedGame.Width, txPasswordedGame.Height),\n                        Color.White);\n                    rightX -= ICON_MARGIN;\n                }\n\n                // skill level icon (shown even if passworded)\n                Texture2D txSkillLevelIcon = txSkillLevelIcons[hostedGame.SkillLevel];\n                if (txSkillLevelIcon != null)\n                {\n                    rightX -= txSkillLevelIcon.Width;\n                    DrawTexture(txSkillLevelIcon,\n                        new Rectangle(rightX, height, txSkillLevelIcon.Width, txSkillLevelIcon.Height),\n                        Color.White);\n                }\n\n                var text = lbItem.Text;\n                if (hostedGame.IsLoadedGame)\n                    text = lbItem.Text + LOADED_GAME_TEXT;\n\n                x += lbItem.TextXPadding;\n\n                DrawStringWithShadow(text, FontIndex,\n                    new Vector2(x, height),\n                    lbItem.TextColor);\n\n                height += LineHeight;\n            }\n\n            if (DrawBorders)\n                DrawPanelBorders();\n\n            DrawChildren(gameTime);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Statistics;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing DTAClient.Domain.Multiplayer;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// An abstract base class for a multiplayer game loading lobby.\n    /// </summary>\n    public abstract class GameLoadingLobbyBase : XNAWindow, ISwitchable\n    {\n        public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordHandler) : base(windowManager)\n        {\n            this.discordHandler = discordHandler;\n        }\n\n        public event EventHandler GameLeft;\n\n        /// <summary>\n        /// The list of players in the current saved game.\n        /// </summary>\n        protected List<SavedGamePlayer> SGPlayers = new List<SavedGamePlayer>();\n\n        /// <summary>\n        /// The list of players in the game lobby.\n        /// </summary>\n        protected List<PlayerInfo> Players = new List<PlayerInfo>();\n\n        protected bool IsHost = false;\n\n        protected DiscordHandler discordHandler;\n\n        protected XNAClientDropDown ddSavedGame;\n\n        protected ChatListBox lbChatMessages;\n        protected XNATextBox tbChatInput;\n\n        protected EnhancedSoundEffect sndGetReadySound;\n        protected EnhancedSoundEffect sndJoinSound;\n        protected EnhancedSoundEffect sndLeaveSound;\n        protected EnhancedSoundEffect sndMessageSound;\n\n        protected XNALabel lblDescription;\n        protected XNAPanel panelPlayers;\n        protected XNALabel[] lblPlayerNames;\n\n        private XNALabel lblMapName;\n        protected XNALabel lblMapNameValue;\n        private XNALabel lblGameMode;\n        protected XNALabel lblGameModeValue;\n        private XNALabel lblSavedGameTime;\n\n        protected XNAClientButton btnLoadGame;\n        protected XNAClientButton btnLeaveGame;\n\n        private List<MultiplayerColor> MPColors = new List<MultiplayerColor>();\n\n        private string loadedGameID;\n\n        private bool isSettingUp = false;\n        private FileSystemWatcher fsw;\n\n        private int uniqueGameId = 0;\n        private DateTime gameLoadTime;\n\n        public override void Initialize()\n        {\n            Name = \"GameLoadingLobby\";\n            ClientRectangle = new Rectangle(0, 0, 590, 510);\n            BackgroundTexture = AssetLoader.LoadTexture(\"loadmpsavebg.png\");\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = nameof(lblDescription);\n            lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0);\n            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\");\n\n            panelPlayers = new XNAPanel(WindowManager);\n            panelPlayers.ClientRectangle = new Rectangle(12, 32, 373, 125);\n            panelPlayers.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            panelPlayers.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n\n            AddChild(lblDescription);\n            AddChild(panelPlayers);\n\n            lblPlayerNames = new XNALabel[8];\n            for (int i = 0; i < 8; i++)\n            {\n                XNALabel lblPlayerName = new XNALabel(WindowManager);\n                lblPlayerName.Name = nameof(lblPlayerName) + i;\n\n                if (i < 4)\n                    lblPlayerName.ClientRectangle = new Rectangle(9, 9 + 30 * i, 0, 0);\n                else\n                    lblPlayerName.ClientRectangle = new Rectangle(190, 9 + 30 * (i - 4), 0, 0);\n\n                lblPlayerName.Text = string.Format(\"Player {0}\".L10N(\"Client:Main:PlayerX\"), i) + \" \";\n                panelPlayers.AddChild(lblPlayerName);\n                lblPlayerNames[i] = lblPlayerName;\n            }\n\n            lblMapName = new XNALabel(WindowManager);\n            lblMapName.Name = nameof(lblMapName);\n            lblMapName.FontIndex = 1;\n            lblMapName.ClientRectangle = new Rectangle(panelPlayers.Right + 12,\n                panelPlayers.Y, 0, 0);\n            lblMapName.Text = \"MAP:\".L10N(\"Client:Main:MapLabel\");\n\n            lblMapNameValue = new XNALabel(WindowManager);\n            lblMapNameValue.Name = nameof(lblMapNameValue);\n            lblMapNameValue.ClientRectangle = new Rectangle(lblMapName.X,\n                lblMapName.Y + 18, 0, 0);\n            lblMapNameValue.Text = \"Map name\".L10N(\"Client:Main:MapName\");\n\n            lblGameMode = new XNALabel(WindowManager);\n            lblGameMode.Name = nameof(lblGameMode);\n            lblGameMode.ClientRectangle = new Rectangle(lblMapName.X,\n                panelPlayers.Y + 40, 0, 0);\n            lblGameMode.FontIndex = 1;\n            lblGameMode.Text = \"GAME MODE:\".L10N(\"Client:Main:GameMode\");\n\n            lblGameModeValue = new XNALabel(WindowManager);\n            lblGameModeValue.Name = nameof(lblGameModeValue);\n            lblGameModeValue.ClientRectangle = new Rectangle(lblGameMode.X,\n                lblGameMode.Y + 18, 0, 0);\n            lblGameModeValue.Text = \"Game mode\".L10N(\"Client:Main:GameModeValueText\");\n\n            lblSavedGameTime = new XNALabel(WindowManager);\n            lblSavedGameTime.Name = nameof(lblSavedGameTime);\n            lblSavedGameTime.ClientRectangle = new Rectangle(lblMapName.X,\n                panelPlayers.Bottom - 40, 0, 0);\n            lblSavedGameTime.FontIndex = 1;\n            lblSavedGameTime.Text = \"SAVED GAME:\".L10N(\"Client:Main:SavedGame\");\n\n            ddSavedGame = new XNAClientDropDown(WindowManager);\n            ddSavedGame.Name = nameof(ddSavedGame);\n            ddSavedGame.ClientRectangle = new Rectangle(lblSavedGameTime.X,\n                panelPlayers.Bottom - 21,\n                Width - lblSavedGameTime.X - 12, 21);\n            ddSavedGame.SelectedIndexChanged += DdSavedGame_SelectedIndexChanged;\n\n            lbChatMessages = new ChatListBox(WindowManager);\n            lbChatMessages.Name = nameof(lbChatMessages);\n            lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbChatMessages.ClientRectangle = new Rectangle(12, panelPlayers.Bottom + 12,\n                Width - 24,\n                Height - panelPlayers.Bottom - 12 - 29 - 34);\n\n            tbChatInput = new XNATextBox(WindowManager);\n            tbChatInput.Name = nameof(tbChatInput);\n            tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X,\n                lbChatMessages.Bottom + 3, lbChatMessages.Width, 19);\n            tbChatInput.MaximumTextLength = 200;\n            tbChatInput.EnterPressed += TbChatInput_EnterPressed;\n\n            btnLoadGame = new XNAClientButton(WindowManager);\n            btnLoadGame.Name = nameof(btnLoadGame);\n            btnLoadGame.ClientRectangle = new Rectangle(lbChatMessages.X,\n                tbChatInput.Bottom + 6, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLoadGame.Text = \"Load Game\".L10N(\"Client:Main:LoadGame\");\n            btnLoadGame.LeftClick += BtnLoadGame_LeftClick;\n\n            btnLeaveGame = new XNAClientButton(WindowManager);\n            btnLeaveGame.Name = nameof(btnLeaveGame);\n            btnLeaveGame.ClientRectangle = new Rectangle(Width - 145,\n                btnLoadGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLeaveGame.Text = \"Leave Game\".L10N(\"Client:Main:LeaveGame\");\n            btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick;\n\n            AddChild(lblMapName);\n            AddChild(lblMapNameValue);\n            AddChild(lblGameMode);\n            AddChild(lblGameModeValue);\n            AddChild(lblSavedGameTime);\n            AddChild(lbChatMessages);\n            AddChild(tbChatInput);\n            AddChild(btnLoadGame);\n            AddChild(btnLeaveGame);\n            AddChild(ddSavedGame);\n\n            base.Initialize();\n\n            sndJoinSound = new EnhancedSoundEffect(\"joingame.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown);\n            sndLeaveSound = new EnhancedSoundEffect(\"leavegame.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown);\n            sndMessageSound = new EnhancedSoundEffect(\"message.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown);\n            sndGetReadySound = new EnhancedSoundEffect(\"getready.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyGetReadyCooldown);\n\n            MPColors = MultiplayerColor.LoadColors();\n\n            WindowManager.CenterControlOnScreen(this);\n\n            if (SavedGameManager.AreSavedGamesAvailable())\n            {\n                fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, \"Saved Games\"), \"*.NET\");\n                fsw.EnableRaisingEvents = false;\n                fsw.Created += fsw_Created;\n                fsw.Changed += fsw_Created;\n            }\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with actual information.\n        /// </summary>\n        /// <param name=\"resetTimer\">Whether to restart the \"Elapsed\" timer or not</param>\n        protected abstract void UpdateDiscordPresence(bool resetTimer = false);\n\n        /// <summary>\n        /// Resets Discord Rich Presence to default state.\n        /// </summary>\n        protected void ResetDiscordPresence() => discordHandler.UpdatePresence();\n\n        private void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGame();\n\n        protected virtual void LeaveGame()\n        {\n            GameLeft?.Invoke(this, EventArgs.Empty);\n            ResetDiscordPresence();\n        }\n\n        private void fsw_Created(object sender, FileSystemEventArgs e) =>\n            AddCallback(new Action<FileSystemEventArgs>(HandleFSWEvent), e);\n\n        private void HandleFSWEvent(FileSystemEventArgs e)\n        {\n            Logger.Log(\"FSW Event: \" + e.FullPath);\n\n            if (Path.GetFileName(e.FullPath) == \"SAVEGAME.NET\")\n            {\n                SavedGameManager.RenameSavedGame();\n            }\n        }\n\n        private void BtnLoadGame_LeftClick(object sender, EventArgs e)\n        {\n            if (!IsHost)\n            {\n                RequestReadyStatus();\n                return;\n            }\n\n            if (Players.Find(p => !p.Ready) != null)\n            {\n                GetReadyNotification();\n                return;\n            }\n\n            if (Players.Count != SGPlayers.Count)\n            {\n                NotAllPresentNotification();\n                return;\n            }\n\n            HostStartGame();\n        }\n\n        protected abstract void RequestReadyStatus();\n\n        protected virtual void GetReadyNotification()\n        {\n            AddNotice(\"The game host wants to load the game but cannot because not all players are ready!\".L10N(\"Client:Main:GetReadyPlease\"));\n\n            if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready)\n                sndGetReadySound.Play();\n#if WINFORMS\n\n            WindowManager.FlashWindow();\n#endif\n        }\n\n        protected virtual void NotAllPresentNotification() =>\n            AddNotice(\"You cannot load the game before all players are present.\".L10N(\"Client:Main:NotAllPresent\"));\n\n        protected abstract void HostStartGame();\n\n        protected void LoadGame()\n        {\n            FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, \"spawn.ini\");\n\n            spawnFileInfo.Delete();\n\n            File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Saved Games\", \"spawnSG.ini\"), spawnFileInfo.FullName);\n\n            IniFile spawnIni = new IniFile(spawnFileInfo.FullName);\n\n            int sgIndex = (ddSavedGame.Items.Count - 1) - ddSavedGame.SelectedIndex;\n\n            spawnIni.SetStringValue(\"Settings\", \"SaveGameName\",\n                string.Format(\"SVGM_{0}.NET\", sgIndex.ToString(\"D3\")));\n            spawnIni.SetBooleanValue(\"Settings\", \"LoadSaveGame\", true);\n\n            PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n\n            if (localPlayer == null)\n                return;\n\n            spawnIni.SetIntValue(\"Settings\", \"Port\", localPlayer.Port);\n\n            for (int i = 1; i < Players.Count; i++)\n            {\n                string otherName = spawnIni.GetStringValue(\"Other\" + i, \"Name\", string.Empty);\n\n                if (string.IsNullOrEmpty(otherName))\n                    continue;\n\n                PlayerInfo otherPlayer = Players.Find(p => p.Name == otherName);\n\n                if (otherPlayer == null)\n                    continue;\n\n                spawnIni.SetStringValue(\"Other\" + i, \"Ip\", otherPlayer.IPAddress);\n                spawnIni.SetIntValue(\"Other\" + i, \"Port\", otherPlayer.Port);\n            }\n\n            WriteSpawnIniAdditions(spawnIni);\n            spawnIni.WriteIniFile();\n\n            FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, \"spawnmap.ini\");\n            spawnMapFileInfo.Delete();\n            using (var spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName))\n            {\n                spawnMapStreamWriter.WriteLine(\"[Map]\");\n                spawnMapStreamWriter.WriteLine(\"Size=0,0,50,50\");\n                spawnMapStreamWriter.WriteLine(\"LocalSize=0,0,50,50\");\n                spawnMapStreamWriter.WriteLine();\n            }\n\n            gameLoadTime = DateTime.Now;\n\n            GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited;\n            GameProcessLogic.StartGameProcess(WindowManager);\n\n            fsw.EnableRaisingEvents = true;\n            UpdateDiscordPresence(true);\n        }\n\n        private void SharedUILogic_GameProcessExited() =>\n            AddCallback(new Action(HandleGameProcessExited), null);\n\n        protected virtual void HandleGameProcessExited()\n        {\n            fsw.EnableRaisingEvents = false;\n\n            GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited;\n\n            var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId);\n\n            if (matchStatistics != null)\n            {\n                int oldLength = matchStatistics.LengthInSeconds;\n                int newLength = matchStatistics.LengthInSeconds +\n                    (int)(DateTime.Now - gameLoadTime).TotalSeconds;\n\n                matchStatistics.ParseStatistics(ProgramConstants.GamePath,\n                    ClientConfiguration.Instance.LocalGame, true);\n\n                matchStatistics.LengthInSeconds = newLength;\n\n                StatisticsManager.Instance.SaveDatabase();\n            }\n            UpdateDiscordPresence(true);\n        }\n\n        protected virtual void WriteSpawnIniAdditions(IniFile spawnIni)\n        {\n            // Do nothing by default\n        }\n\n        protected void AddNotice(string notice) => AddNotice(notice, Color.White);\n\n        protected abstract void AddNotice(string message, Color color);\n\n        /// <summary>\n        /// Refreshes the UI  based on the latest saved game\n        /// and information in the saved spawn.ini file, as well\n        /// as information on whether the local player is the host of the game.\n        /// </summary>\n        public virtual void Refresh(bool isHost)\n        {\n            isSettingUp = true;\n            IsHost = isHost;\n\n            SGPlayers.Clear();\n            Players.Clear();\n            ddSavedGame.Items.Clear();\n            lbChatMessages.Clear();\n            lbChatMessages.TopIndex = 0;\n\n            ddSavedGame.AllowDropDown = isHost;\n            btnLoadGame.Text = isHost ? \"Load Game\".L10N(\"Client:Main:ButtonLoadGame\") : \"I'm Ready\".L10N(\"Client:Main:ButtonGetReady\");\n\n            IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"Saved Games\", \"spawnSG.ini\"));\n\n            loadedGameID = spawnSGIni.GetStringValue(\"Settings\", \"GameID\", \"0\");\n            lblMapNameValue.Tag = spawnSGIni.GetStringValue(\"Settings\", \"UIMapName\", string.Empty);\n            lblMapNameValue.Text = ((string)lblGameModeValue.Tag).L10N($\"INI:Maps:{spawnSGIni.GetStringValue(\"Settings\", \"MapID\", string.Empty)}:Description\");\n            lblGameModeValue.Tag = spawnSGIni.GetStringValue(\"Settings\", \"UIGameMode\", string.Empty);\n            lblGameModeValue.Text = ((string)lblGameModeValue.Tag).L10N($\"INI:GameModes:{(string)lblGameModeValue.Tag}:UIName\");\n\n            uniqueGameId = spawnSGIni.GetIntValue(\"Settings\", \"GameID\", -1);\n\n            int playerCount = spawnSGIni.GetIntValue(\"Settings\", \"PlayerCount\", 0);\n\n            SavedGamePlayer localPlayer = new SavedGamePlayer();\n            localPlayer.Name = ProgramConstants.PLAYERNAME;\n            localPlayer.ColorIndex = MPColors.FindIndex(\n                c => c.GameColorIndex == spawnSGIni.GetIntValue(\"Settings\", \"Color\", 0));\n\n            SGPlayers.Add(localPlayer);\n\n            for (int i = 1; i < playerCount; i++)\n            {\n                string sectionName = \"Other\" + i;\n\n                SavedGamePlayer sgPlayer = new SavedGamePlayer();\n                sgPlayer.Name = spawnSGIni.GetStringValue(sectionName, \"Name\", \"Unknown player\".L10N(\"Client:Main:UnknownPlayer\"));\n                sgPlayer.ColorIndex = MPColors.FindIndex(\n                    c => c.GameColorIndex == spawnSGIni.GetIntValue(sectionName, \"Color\", 0));\n\n                SGPlayers.Add(sgPlayer);\n            }\n\n            for (int i = 0; i < SGPlayers.Count; i++)\n            {\n                lblPlayerNames[i].Enabled = true;\n                lblPlayerNames[i].Visible = true;\n            }\n\n            for (int i = SGPlayers.Count; i < 8; i++)\n            {\n                lblPlayerNames[i].Enabled = false;\n                lblPlayerNames[i].Visible = false;\n            }\n\n            List<string> timestamps = SavedGameManager.GetSaveGameTimestamps();\n            timestamps.Reverse(); // Most recent saved game first\n\n            timestamps.ForEach(ts => ddSavedGame.AddItem(ts));\n\n            if (ddSavedGame.Items.Count > 0)\n                ddSavedGame.SelectedIndex = 0;\n\n            CopyPlayerDataToUI();\n            isSettingUp = false;\n        }\n\n        protected void CopyPlayerDataToUI()\n        {\n            for (int i = 0; i < SGPlayers.Count; i++)\n            {\n                SavedGamePlayer sgPlayer = SGPlayers[i];\n\n                PlayerInfo pInfo = Players.Find(p => p.Name == SGPlayers[i].Name);\n\n                XNALabel playerLabel = lblPlayerNames[i];\n\n                if (pInfo == null)\n                {\n                    playerLabel.RemapColor = Color.Gray;\n                    playerLabel.Text = sgPlayer.Name + \" \" + \"(Not present)\".L10N(\"Client:Main:NotPresentSuffix\");\n                    continue;\n                }\n\n                playerLabel.RemapColor = sgPlayer.ColorIndex > -1 ? MPColors[sgPlayer.ColorIndex].XnaColor\n                    : Color.White;\n                playerLabel.Text = pInfo.Ready ? sgPlayer.Name : sgPlayer.Name + \" \" + \"(Not Ready)\".L10N(\"Client:Main:NotReadySuffix\");\n            }\n        }\n\n        protected virtual string GetIPAddressForPlayer(PlayerInfo pInfo) => \"0.0.0.0\";\n\n        private void DdSavedGame_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (!IsHost)\n                return;\n\n            for (int i = 1; i < Players.Count; i++)\n                Players[i].Ready = false;\n\n            CopyPlayerDataToUI();\n\n            if (!isSettingUp)\n                BroadcastOptions();\n            UpdateDiscordPresence();\n        }\n\n        private void TbChatInput_EnterPressed(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbChatInput.Text))\n                return;\n\n            SendChatMessage(tbChatInput.Text);\n            tbChatInput.Text = string.Empty;\n        }\n\n        /// <summary>\n        /// Override in a derived class to broadcast player ready statuses and the selected\n        /// saved game to players.\n        /// </summary>\n        protected abstract void BroadcastOptions();\n\n        protected abstract void SendChatMessage(string message);\n\n        public override void Draw(GameTime gameTime)\n        {\n            Renderer.FillRectangle(new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY),\n                new Color(0, 0, 0, 255));\n\n            base.Draw(gameTime);\n        }\n\n        public void SwitchOn() => Enable();\n\n        public void SwitchOff() => Disable();\n\n        public abstract string GetSwitchName();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/ChatBoxCommand.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    /// <summary>\n    /// A command that can be executed by typing a message starting with / on\n    /// a multiplayer game lobby's chat box.\n    /// </summary>\n    public class ChatBoxCommand\n    {\n        public ChatBoxCommand(string command, string description, bool hostOnly, Action<string> action)\n        {\n            Command = command;\n            Description = description;\n            HostOnly = hostOnly;\n            Action = action;\n        }\n\n        public string Command { get; private set; }\n        public string Description { get; private set; }\n        public bool HostOnly { get; private set; }\n        public Action<string> Action { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs",
    "content": "using ClientCore;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain;\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers;\nusing DTAClient.Online;\nusing DTAClient.Online.EventArguments;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Buffers.Binary;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class CnCNetGameLobby : MultiplayerGameLobby\n    {\n        private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3;\n        private const int AI_PLAYER_OPTIONS_LENGTH = 2;\n\n        private const double GAME_BROADCAST_INTERVAL = 30.0;\n        private const double GAME_BROADCAST_ACCELERATION = 10.0;\n        private const double INITIAL_GAME_BROADCAST_DELAY = 10.0;\n\n        private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow;\n\n        private const string MAP_SHARING_FAIL_MESSAGE = \"MAPFAIL\";\n        private const string MAP_SHARING_DOWNLOAD_REQUEST = \"MAPOK\";\n        private const string MAP_SHARING_UPLOAD_REQUEST = \"MAPREQ\";\n        private const string MAP_SHARING_DISABLED_MESSAGE = \"MAPSDISABLED\";\n        private const string CHEAT_DETECTED_MESSAGE = \"CD\";\n        private const string DICE_ROLL_MESSAGE = \"DR\";\n        private const string CHANGE_TUNNEL_SERVER_MESSAGE = \"CHTNL\";\n\n        public CnCNetGameLobby(\n            WindowManager windowManager, \n            TopBar topBar, \n            CnCNetManager connectionManager,\n            TunnelHandler tunnelHandler, \n            GameCollection gameCollection, \n            CnCNetUserData cncnetUserData, \n            MapLoader mapLoader, \n            DiscordHandler discordHandler,\n            PrivateMessagingWindow pmWindow,\n            Random random\n        ) : base(windowManager, \"MultiplayerGameLobby\", topBar, mapLoader, discordHandler, pmWindow, random)\n        {\n            this.connectionManager = connectionManager;\n            localGame = ClientConfiguration.Instance.LocalGame;\n            this.tunnelHandler = tunnelHandler;\n            this.gameCollection = gameCollection;\n            this.cncnetUserData = cncnetUserData;\n            this.pmWindow = pmWindow;\n            this.random = random;\n            \n            gameHostInactiveChecker = ClientConfiguration.Instance.InactiveHostKickEnabled? new GameHostInactiveChecker(WindowManager) : null;\n\n            ctcpCommandHandlers = new CommandHandlerBase[]\n            {\n                new IntCommandHandler(\"OR\", HandleOptionsRequest),\n                new IntCommandHandler(\"R\", HandleReadyRequest),\n                new StringCommandHandler(\"PO\", ApplyPlayerOptions),\n                new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions),\n                new StringCommandHandler(\"GO\", ApplyGameOptions),\n                new StringCommandHandler(\"START\", NonHostLaunchGame),\n                new NotificationHandler(\"AISPECS\", HandleNotification, AISpectatorsNotification),\n                new NotificationHandler(\"GETREADY\", HandleNotification, GetReadyNotification),\n                new NotificationHandler(\"INSFSPLRS\", HandleNotification, InsufficientPlayersNotification),\n                new NotificationHandler(\"TMPLRS\", HandleNotification, TooManyPlayersNotification),\n                new NotificationHandler(\"CLRS\", HandleNotification, SharedColorsNotification),\n                new NotificationHandler(\"SLOC\", HandleNotification, SharedStartingLocationNotification),\n                new NotificationHandler(\"LCKGME\", HandleNotification, LockGameNotification),\n                new IntNotificationHandler(\"NVRFY\", HandleIntNotification, NotVerifiedNotification),\n                new IntNotificationHandler(\"INGM\", HandleIntNotification, StillInGameNotification),\n                new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest),\n                new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage),\n                new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest),\n                new NoParamCommandHandler(MAP_SHARING_DISABLED_MESSAGE, HandleMapSharingBlockedMessage),\n                new NoParamCommandHandler(\"STRTD\", GameStartedNotification),\n                new NoParamCommandHandler(\"RETURN\", ReturnNotification),\n                new IntCommandHandler(\"TNLPNG\", HandleTunnelPing),\n                new StringCommandHandler(\"FHSH\", FileHashNotification),\n                new StringCommandHandler(\"MM\", CheaterNotification),\n                new StringCommandHandler(DICE_ROLL_MESSAGE, HandleDiceRollResult),\n                new NoParamCommandHandler(CHEAT_DETECTED_MESSAGE, HandleCheatDetectedMessage),\n                new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage),\n                new StringCommandHandler(\"GSETTINGS\", ApplyGameLobbySettings)\n            };\n\n            MapSharer.MapDownloadFailed += MapSharer_MapDownloadFailed;\n            MapSharer.MapDownloadComplete += MapSharer_MapDownloadComplete;\n            MapSharer.MapUploadFailed += MapSharer_MapUploadFailed;\n            MapSharer.MapUploadComplete += MapSharer_MapUploadComplete;\n\n            AddChatBoxCommand(new ChatBoxCommand(\"TUNNELINFO\",\n                \"View tunnel server information\".L10N(\"Client:Main:TunnelInfoCommand\"), false, PrintTunnelServerInformation));\n            AddChatBoxCommand(new ChatBoxCommand(\"CHANGETUNNEL\",\n                \"Change the used CnCNet tunnel server (game host only)\".L10N(\"Client:Main:ChangeTunnelCommand\"),\n                true, (s) => ShowTunnelSelectionWindow(\"Select tunnel server:\".L10N(\"Client:Main:SelectTunnelServerCommand\"))));\n            AddChatBoxCommand(new ChatBoxCommand(\"DOWNLOADMAP\",\n                \"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\"),\n                true, DownloadMapByIdCommand));\n        }\n\n        public event EventHandler GameLeft;\n\n        private TunnelHandler tunnelHandler;\n        private TunnelSelectionWindow tunnelSelectionWindow;\n        private GameLobbySettingsWindow gameLobbySettingsWindow;\n        private XNAClientButton btnChangeTunnel;\n        private XNAClientButton btnGameLobbySettings;\n\n        private Channel channel;\n        private CnCNetManager connectionManager;\n        private string localGame;\n\n        private readonly GameHostInactiveChecker gameHostInactiveChecker;\n\n        private GameCollection gameCollection;\n        private CnCNetUserData cncnetUserData;\n        private readonly PrivateMessagingWindow pmWindow;\n        private GlobalContextMenu globalContextMenu;\n\n        private string hostName;\n\n        private CommandHandlerBase[] ctcpCommandHandlers;\n\n        private IRCColor chatColor;\n\n        private XNATimerControl gameBroadcastTimer;\n\n        private int playerLimit;\n\n        protected override int MaxPlayerCount => playerLimit;\n\n        private bool closed = false;\n\n        private int skillLevel = ClientConfiguration.Instance.DefaultSkillLevelIndex;\n\n        private string gameRoomName;\n\n        private bool isCustomPassword = false;\n\n        private string gameFilesHash;\n\n        private List<string> hostUploadedMaps = new List<string>();\n        private List<string> chatCommandDownloadedMaps = new List<string>();\n\n        private MapSharingConfirmationPanel mapSharingConfirmationPanel;\n\n        private Random random;\n\n        /// <summary>\n        /// The SHA1 of the latest selected map.\n        /// Used for map sharing.\n        /// </summary>\n        private string lastMapSHA1;\n\n        /// <summary>\n        /// The map name of the latest selected map.\n        /// Used for map sharing.\n        /// </summary>\n        private string lastMapName;\n\n        /// <summary>\n        /// The game mode of the latest selected map.\n        /// Used for map sharing.\n        /// </summary>\n        private string lastGameMode;\n\n        /// <summary>\n        /// Set to true if host has selected invalid tunnel server.\n        /// </summary>\n        private bool tunnelErrorMode;\n\n        public override void Initialize()\n        {\n            IniNameOverride = nameof(CnCNetGameLobby);\n            base.Initialize();\n\n            if (gameHostInactiveChecker != null)\n            {\n                MouseMove += (sender, args) => gameHostInactiveChecker.Reset();\n                gameHostInactiveChecker.CloseEvent += GameHostInactiveChecker_CloseEvent;\n            }\n\n            btnChangeTunnel = FindChild<XNAClientButton>(nameof(btnChangeTunnel));\n            btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick;\n\n            btnGameLobbySettings = FindChild<XNAClientButton>(nameof(btnGameLobbySettings), optional: true);\n            btnGameLobbySettings?.LeftClick += BtnGameLobbySettings_LeftClick;\n\n            gameBroadcastTimer = new XNATimerControl(WindowManager);\n            gameBroadcastTimer.AutoReset = true;\n            gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL);\n            gameBroadcastTimer.Enabled = false;\n            gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed;\n\n            tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler);\n            tunnelSelectionWindow.Initialize();\n            tunnelSelectionWindow.DrawOrder = 1;\n            tunnelSelectionWindow.UpdateOrder = 1;\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow);\n            tunnelSelectionWindow.CenterOnParent();\n            tunnelSelectionWindow.Disable();\n            tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected;\n\n            gameLobbySettingsWindow = new GameLobbySettingsWindow(WindowManager);\n            gameLobbySettingsWindow.Initialize();\n            gameLobbySettingsWindow.DrawOrder = 1;\n            gameLobbySettingsWindow.UpdateOrder = 1;\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, gameLobbySettingsWindow);\n            gameLobbySettingsWindow.CenterOnParent();\n            gameLobbySettingsWindow.Disable();\n            gameLobbySettingsWindow.SettingsChanged += GameLobbySettingsWindow_SettingsChanged;\n\n            MapLoader.MapChanged += MapLoader_MapChanged;\n            mapSharingConfirmationPanel = new MapSharingConfirmationPanel(WindowManager);\n            MapPreviewBox.AddChild(mapSharingConfirmationPanel);\n            mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed;\n\n            WindowManager.AddAndInitializeControl(gameBroadcastTimer);\n\n            globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow);\n            AddChild(globalContextMenu);\n\n            MultiplayerNameRightClicked += MultiplayerName_RightClick;\n\n            PostInitialize();\n        }\n\n        private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args)\n        {\n            globalContextMenu.Show(new GlobalContextMenuData()\n            {\n                PlayerName = args.PlayerName,\n                PreventJoinGame = true\n            }, GetCursorPoint());\n        }\n\n        private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow(\"Select tunnel server:\".L10N(\"Client:Main:SelectTunnelServer\"));\n\n        private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame();\n\n        public void SetUp(Channel channel, bool isHost, int playerLimit,\n            CnCNetTunnel tunnel, string hostName, bool isCustomPassword,\n            int skillLevel)\n        {\n            this.channel = channel;\n            channel.MessageAdded += Channel_MessageAdded;\n            channel.CTCPReceived += Channel_CTCPReceived;\n            channel.UserKicked += Channel_UserKicked;\n            channel.UserQuitIRC += Channel_UserQuitIRC;\n            channel.UserLeft += Channel_UserLeft;\n            channel.UserAdded += Channel_UserAdded;\n            channel.UserNameChanged += Channel_UserNameChanged;\n            channel.UserListReceived += Channel_UserListReceived;\n\n            this.hostName = hostName;\n            this.playerLimit = playerLimit;\n            this.isCustomPassword = isCustomPassword;\n            this.skillLevel = skillLevel;\n            this.gameRoomName = channel.UIName;\n\n            if (isHost)\n            {\n                RandomSeed = random.Next();\n                RefreshMapSelectionUI();\n                btnChangeTunnel.Enable();\n                btnGameLobbySettings?.Enable();\n                StartInactiveCheck();\n            }\n            else\n            {\n                channel.ChannelModesChanged += Channel_ChannelModesChanged;\n                AIPlayers.Clear();\n                btnChangeTunnel.Disable();\n                btnGameLobbySettings?.Disable();\n            }\n\n            tunnelHandler.CurrentTunnel = tunnel;\n            tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged;\n\n            connectionManager.ConnectionLost += ConnectionManager_ConnectionLost;\n            connectionManager.Disconnected += ConnectionManager_Disconnected;\n\n            Refresh(isHost);\n        }\n\n        private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePing();\n\n        private void GameHostInactiveChecker_CloseEvent(object sender, EventArgs e) => LeaveGameLobby();\n\n        public void StartInactiveCheck()\n        {\n            if (isCustomPassword)\n                return;\n\n            gameHostInactiveChecker?.Start();\n        }\n\n        public void StopInactiveCheck() => gameHostInactiveChecker?.Stop();\n\n        public void OnJoined()\n        {\n            FileHashCalculator fhc = new FileHashCalculator();\n            fhc.CalculateHashes();\n\n            gameFilesHash = fhc.GetCompleteHash();\n\n            if (IsHost)\n            {\n                connectionManager.SendCustomMessage(new QueuedMessage(\n                    string.Format(\"MODE {0} +klnNs {1} {2}\", channel.ChannelName,\n                    channel.Password, playerLimit),\n                    QueuedMessageType.SYSTEM_MESSAGE, 50));\n\n                connectionManager.SendCustomMessage(new QueuedMessage(\n                    string.Format(\"TOPIC {0} :{1}\", channel.ChannelName,\n                    ProgramConstants.CNCNET_PROTOCOL_REVISION + \";\" + localGame.ToLower()),\n                    QueuedMessageType.SYSTEM_MESSAGE, 50));\n\n                gameBroadcastTimer.Enabled = true;\n                gameBroadcastTimer.Start();\n                gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY));\n            }\n            else\n            {\n                channel.SendCTCPMessage(\"FHSH \" + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10);\n            }\n\n            TopBar.AddPrimarySwitchable(this);\n            TopBar.SwitchToPrimary();\n            WindowManager.SelectedControl = tbChatInput;\n            ResetAutoReadyCheckbox();\n            UpdatePing();\n            UpdateDiscordPresence(true);\n        }\n\n        private void UpdatePing()\n        {\n            if (tunnelHandler.CurrentTunnel == null)\n                return;\n\n            channel.SendCTCPMessage(\"TNLPNG \" + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10);\n\n            PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME));\n            if (pInfo != null)\n            {\n                pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs;\n                UpdatePlayerPingIndicator(pInfo);\n            }\n        }\n\n        protected override void CopyPlayerDataToUI()\n        {\n            base.CopyPlayerDataToUI();\n\n            for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++)\n            {\n                StatusIndicators[i].SwitchTexture(\n                    i < playerLimit ? PlayerSlotState.Empty : PlayerSlotState.Unavailable);\n            }\n        }\n\n        private void PrintTunnelServerInformation(string s)\n        {\n            if (tunnelHandler.CurrentTunnel == null)\n            {\n                AddNotice(\"Tunnel server unavailable!\".L10N(\"Client:Main:TunnelUnavailable\"));\n            }\n            else\n            {\n                AddNotice(string.Format(\"Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})\".L10N(\"Client:Main:TunnelInfo\"),\n                        tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official\n                    ));\n            }\n        }\n\n        private void ShowTunnelSelectionWindow(string description)\n        {\n            tunnelSelectionWindow.Open(description,\n                tunnelHandler.CurrentTunnel?.Address);\n        }\n\n        private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e)\n        {\n            channel.SendCTCPMessage($\"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}\",\n                QueuedMessageType.SYSTEM_MESSAGE, 10);\n            HandleTunnelServerChange(e.Tunnel);\n        }\n\n        private void BtnGameLobbySettings_LeftClick(object sender, EventArgs e)\n        {\n            if (!IsHost)\n                return;\n\n            string displayPassword = isCustomPassword ? channel.Password : string.Empty;\n            gameLobbySettingsWindow.Open(gameRoomName, playerLimit, skillLevel, displayPassword);\n        }\n\n        private void GameLobbySettingsWindow_SettingsChanged(object sender, GameLobbySettingsEventArgs e)\n        {\n            if (!IsHost)\n                return;\n\n            UpdateGameLobbySettings(e.GameRoomName, e.MaxPlayers, e.SkillLevel, e.Password);\n        }\n\n        private void UpdateGameLobbySettings(string newGameRoomName, int newMaxPlayers, int newSkillLevel, string newPassword)\n        {\n            if (!IsHost)\n                return;\n\n            bool gameNameChanged = gameRoomName != newGameRoomName;\n            bool maxPlayersChanged = playerLimit != newMaxPlayers;\n            bool skillLevelChanged = skillLevel != newSkillLevel;\n\n            string currentUserPassword = isCustomPassword ? channel.Password : string.Empty;\n            bool passwordChanged = currentUserPassword != newPassword;\n\n            // ensure max players isn't less than current player count\n            if (newMaxPlayers < Players.Count + AIPlayers.Count)\n            {\n                AddNotice(string.Format(\"Cannot reduce maximum players to {0} with {1} players currently in game.\"\n                    .L10N(\"Client:Main:CannotReduceMaxPlayers\"), newMaxPlayers, Players.Count + AIPlayers.Count));\n                return;\n            }\n\n            string oldGameRoomName = gameRoomName;\n            bool oldIsCustomPassword = isCustomPassword;\n            gameRoomName = newGameRoomName;\n            channel.UIName = newGameRoomName;\n            playerLimit = newMaxPlayers;\n            skillLevel = newSkillLevel;\n\n            if (passwordChanged)\n            {\n                // if new password is empty, generate password from channel name\n                string actualNewPassword = newPassword;\n                if (string.IsNullOrEmpty(newPassword))\n                {\n                    actualNewPassword = Utilities.CalculateSHA1ForString(channel.ChannelName).Substring(0, 10);\n                    isCustomPassword = false;\n                }\n                else\n                {\n                    isCustomPassword = true;\n                }\n\n                channel.ChangePassword(actualNewPassword, 10);\n            }\n\n            BroadcastGameLobbySettings();\n\n            if (gameNameChanged)\n            {\n                AddNotice(string.Format(\"Game room name changed from \\\"{0}\\\" to \\\"{1}\\\".\"\n                    .L10N(\"Client:Main:GameNameChanged\"), oldGameRoomName, gameRoomName));\n            }\n\n            if (maxPlayersChanged)\n            {\n                CopyPlayerDataToUI();\n                AddNotice(string.Format(\"Maximum players changed to {0}.\"\n                    .L10N(\"Client:Main:MaxPlayersChanged\"), newMaxPlayers));\n            }\n\n            if (skillLevelChanged)\n            {\n                string[] skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n                string skillLevelName = skillLevelOptions[newSkillLevel];\n                string localizedSkillLevel = skillLevelName.L10N($\"INI:ClientDefinitions:SkillLevel:{newSkillLevel}\");\n                AddNotice(string.Format(\"Skill level changed to {0}.\"\n                    .L10N(\"Client:Main:SkillLevelChanged\"), localizedSkillLevel));\n            }\n\n            if (passwordChanged)\n            {\n                if (string.IsNullOrEmpty(newPassword))\n                    AddNotice(\"Password removed from the game.\".L10N(\"Client:Main:PasswordRemoved\"));\n                else if (!oldIsCustomPassword)\n                    AddNotice(\"Password added to the game.\".L10N(\"Client:Main:PasswordAdded\"));\n                else\n                    AddNotice(\"Password changed.\".L10N(\"Client:Main:PasswordChanged\"));\n            }\n\n            BroadcastGame();\n        }\n\n        private void BroadcastGameLobbySettings()\n        {\n            if (!IsHost)\n                return;\n\n            StringBuilder sb = new StringBuilder(\"GSETTINGS \");\n            sb.Append(gameRoomName);\n            sb.Append(\";\");\n            sb.Append(playerLimit);\n            sb.Append(\";\");\n            sb.Append(skillLevel);\n            sb.Append(\";\");\n            sb.Append(Convert.ToInt32(isCustomPassword));\n\n            channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11);\n        }\n\n        private void ApplyGameLobbySettings(string sender, string message)\n        {\n            if (IsHost)\n                return;\n\n            string[] parts = message.Split(';');\n\n            if (parts.Length < 4)\n                return;\n\n            string newGameRoomName = parts[0];\n            int newMaxPlayers = Conversions.IntFromString(parts[1], playerLimit);\n            int newSkillLevel = Conversions.IntFromString(parts[2], skillLevel);\n            bool newIsCustomPassword = Convert.ToBoolean(Conversions.IntFromString(parts[3], 0));\n\n            bool gameNameChanged = gameRoomName != newGameRoomName;\n            bool maxPlayersChanged = playerLimit != newMaxPlayers;\n            bool skillLevelChanged = skillLevel != newSkillLevel;\n\n            gameRoomName = newGameRoomName;\n            channel.UIName = newGameRoomName;\n            playerLimit = newMaxPlayers;\n            skillLevel = newSkillLevel;\n            isCustomPassword = newIsCustomPassword;\n\n            if (gameNameChanged)\n            {\n                AddNotice(string.Format(\"{0} changed game room name to \\\"{1}\\\".\"\n                    .L10N(\"Client:Main:HostChangedGameName\"), sender, gameRoomName));\n            }\n\n            if (maxPlayersChanged)\n            {\n                CopyPlayerDataToUI();\n                AddNotice(string.Format(\"{0} changed maximum players to {1}.\"\n                    .L10N(\"Client:Main:HostChangedMaxPlayers\"), sender, newMaxPlayers));\n            }\n\n            if (skillLevelChanged)\n            {\n                string[] skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n                string skillLevelName = skillLevelOptions[newSkillLevel];\n                string localizedSkillLevel = skillLevelName.L10N($\"INI:ClientDefinitions:SkillLevel:{newSkillLevel}\");\n                AddNotice(string.Format(\"{0} changed skill level to {1}.\"\n                    .L10N(\"Client:Main:HostChangedSkillLevel\"), sender, localizedSkillLevel));\n            }\n        }\n\n        public void ChangeChatColor(IRCColor chatColor)\n        {\n            this.chatColor = chatColor;\n            tbChatInput.TextColor = chatColor.XnaColor;\n        }\n\n        public override void Clear()\n        {\n            base.Clear();\n\n            if (channel != null)\n            {\n                channel.MessageAdded -= Channel_MessageAdded;\n                channel.CTCPReceived -= Channel_CTCPReceived;\n                channel.UserKicked -= Channel_UserKicked;\n                channel.UserQuitIRC -= Channel_UserQuitIRC;\n                channel.UserLeft -= Channel_UserLeft;\n                channel.UserAdded -= Channel_UserAdded;\n                channel.UserNameChanged -= Channel_UserNameChanged;\n                channel.UserListReceived -= Channel_UserListReceived;\n\n                if (!IsHost)\n                {\n                    channel.ChannelModesChanged -= Channel_ChannelModesChanged;\n                }\n\n                connectionManager.RemoveChannel(channel);\n            }\n\n            Disable();\n            PlayerExtraOptionsPanel?.Disable();\n\n            connectionManager.ConnectionLost -= ConnectionManager_ConnectionLost;\n            connectionManager.Disconnected -= ConnectionManager_Disconnected;\n\n            gameBroadcastTimer.Enabled = false;\n            closed = false;\n\n            tbChatInput.Text = string.Empty;\n\n            tunnelHandler.CurrentTunnel = null;\n            tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged;\n\n            if (MapLoader != null)\n                MapLoader.MapChanged -= MapLoader_MapChanged;\n\n            GameLeft?.Invoke(this, EventArgs.Empty);\n\n            TopBar.RemovePrimarySwitchable(this);\n            ResetDiscordPresence();\n        }\n\n        public void LeaveGameLobby()\n        {\n            if (IsHost)\n            {\n                StopInactiveCheck();\n                closed = true;\n                BroadcastGame();\n            }\n\n            Clear();\n            channel?.Leave();\n        }\n\n        private void ConnectionManager_Disconnected(object sender, EventArgs e) => HandleConnectionLoss();\n\n        private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => HandleConnectionLoss();\n\n        private void HandleConnectionLoss()\n        {\n            Clear();\n            Disable();\n        }\n\n        private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e)\n        {\n            Logger.Log(\"CnCNetGameLobby: Nickname change: \" + e.OldUserName + \" to \" + e.User.Name);\n            int index = Players.FindIndex(p => p.Name == e.OldUserName);\n            if (index > -1)\n            {\n                PlayerInfo player = Players[index];\n                player.Name = e.User.Name;\n                ddPlayerNames[index].Items[0].Text = player.Name;\n                AddNotice(string.Format(\"Player {0} changed their name to {1}\".L10N(\"Client:Main:PlayerRename\"), e.OldUserName, e.User.Name));\n            }\n        }\n\n        protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGameLobby();\n\n        protected override void UpdateDiscordPresence(bool resetTimer = false)\n        {\n            if (discordHandler == null)\n                return;\n\n            PlayerInfo player = FindLocalPlayer();\n            if (player == null || Map == null || GameMode == null)\n                return;\n            string side = \"\";\n            if (ddPlayerSides.Length > Players.IndexOf(player))\n                side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag;\n            string currentState = ProgramConstants.IsInGame ? \"In Game\" : \"In Lobby\"; // not UI strings\n\n            discordHandler.UpdatePresence(\n                Map.UntranslatedName, GameMode.UntranslatedUIName, \"Multiplayer\",\n                currentState, Players.Count, playerLimit, side,\n                channel.UIName, IsHost, isCustomPassword, Locked, resetTimer);\n        }\n\n        private void Channel_UserQuitIRC(object sender, UserNameEventArgs e)\n        {\n            RemovePlayer(e.UserName);\n\n            if (e.UserName == hostName)\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(\n                    ERROR_MESSAGE_COLOR, \"The game host abandoned the game.\".L10N(\"Client:Main:HostAbandoned\")));\n                BtnLeaveGame_LeftClick(this, EventArgs.Empty);\n            }\n            else\n                UpdateDiscordPresence();\n        }\n\n        private void Channel_UserLeft(object sender, UserNameEventArgs e)\n        {\n            RemovePlayer(e.UserName);\n\n            if (e.UserName == hostName)\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(\n                    ERROR_MESSAGE_COLOR, \"The game host abandoned the game.\".L10N(\"Client:Main:HostAbandoned\")));\n                BtnLeaveGame_LeftClick(this, EventArgs.Empty);\n            }\n            else\n                UpdateDiscordPresence();\n        }\n\n        private void Channel_UserKicked(object sender, UserNameEventArgs e)\n        {\n            if (e.UserName == ProgramConstants.PLAYERNAME)\n            {\n                connectionManager.MainChannel.AddMessage(new ChatMessage(\n                    ERROR_MESSAGE_COLOR, \"You were kicked from the game!\".L10N(\"Client:Main:YouWereKicked\")));\n                Clear();\n                this.Visible = false;\n                this.Enabled = false;\n                return;\n            }\n\n            int index = Players.FindIndex(p => p.Name == e.UserName);\n\n            if (index > -1)\n            {\n                Players.RemoveAt(index);\n                CopyPlayerDataToUI();\n                UpdateDiscordPresence();\n                ClearReadyStatuses();\n            }\n        }\n\n        private void Channel_UserListReceived(object sender, EventArgs e)\n        {\n            if (!IsHost)\n            {\n                if (channel.Users.Find(hostName) == null)\n                {\n                    connectionManager.MainChannel.AddMessage(new ChatMessage(\n                        ERROR_MESSAGE_COLOR, \"The game host has abandoned the game.\".L10N(\"Client:Main:HostHasAbandoned\")));\n                    BtnLeaveGame_LeftClick(this, EventArgs.Empty);\n                }\n            }\n            UpdateDiscordPresence();\n        }\n\n        private void Channel_UserAdded(object sender, ChannelUserEventArgs e)\n        {\n            PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name);\n            Players.Add(pInfo);\n\n            if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0)\n                AIPlayers.RemoveAt(AIPlayers.Count - 1);\n\n            sndJoinSound.Play();\n#if WINFORMS\n            WindowManager.FlashWindow();\n#endif\n\n            if (!IsHost)\n            {\n                CopyPlayerDataToUI();\n                return;\n            }\n\n            if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME)\n            {\n                // Changing the map applies forced settings (co-op sides etc.) to the\n                // new player, and it also sends an options broadcast message\n                //CopyPlayerDataToUI(); This is also called by ChangeMap()\n                ChangeMap(GameModeMap);\n                BroadcastPlayerOptions();\n                BroadcastPlayerExtraOptions();\n                UpdateDiscordPresence();\n            }\n            else\n            {\n                Players[0].Ready = true;\n                CopyPlayerDataToUI();\n            }\n\n            if (Players.Count >= playerLimit)\n            {\n                AddNotice(\"Player limit reached. The game room has been locked.\".L10N(\"Client:Main:GameRoomNumberLimitReached\"));\n                LockGame();\n            }\n        }\n\n        private void RemovePlayer(string playerName)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == playerName);\n\n            if (pInfo != null)\n            {\n                Players.Remove(pInfo);\n\n                CopyPlayerDataToUI();\n\n                // This might not be necessary\n                if (IsHost)\n                    BroadcastPlayerOptions();\n            }\n\n            sndLeaveSound.Play();\n\n            if (IsHost && Locked && !ProgramConstants.IsInGame)\n            {\n                UnlockGame(true);\n            }\n        }\n\n        private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e)\n        {\n            if (e.ModeString == \"+i\")\n            {\n                if (Players.Count >= playerLimit)\n                    AddNotice(\"Player limit reached. The game room has been locked.\".L10N(\"Client:Main:GameRoomNumberLimitReached\"));\n                else\n                    AddNotice(\"The game host has locked the game room.\".L10N(\"Client:Main:RoomLockedByHost\"));\n                Locked = true;\n            }\n            else if (e.ModeString == \"-i\")\n            {\n                AddNotice(\"The game room has been unlocked.\".L10N(\"Client:Main:GameRoomUnlocked\"));\n                Locked = false;\n            }\n        }\n\n        private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e)\n        {\n            Logger.Log(\"CnCNetGameLobby_CTCPReceived\");\n\n            foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers)\n            {\n                if (cmdHandler.Handle(e.UserName, e.Message))\n                {\n                    UpdateDiscordPresence();\n                    return;\n                }\n            }\n\n            Logger.Log(\"Unhandled CTCP command: \" + e.Message + \" from \" + e.UserName);\n        }\n\n        private void Channel_MessageAdded(object sender, IRCMessageEventArgs e)\n        {\n            if (cncnetUserData.IsIgnored(e.Message.SenderIdent))\n            {\n                lbChatMessages.AddMessage(new ChatMessage(Color.Silver,\n                    string.Format(\"Message blocked from {0}\".L10N(\"Client:Main:MessageBlockedFromPlayer\"), e.Message.SenderName)));\n            }\n            else\n            {\n                lbChatMessages.AddMessage(e.Message);\n\n                if (e.Message.SenderName != null)\n                    sndMessageSound.Play();\n            }\n        }\n\n        /// <summary>\n        /// Starts the game for the game host.\n        /// </summary>\n        protected override void HostLaunchGame()\n        {\n            if (Players.Count > 1)\n            {\n                AddNotice(\"Contacting tunnel server...\".L10N(\"Client:Main:ConnectingTunnel\"));\n\n                List<int> playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count);\n\n                if (playerPorts.Count < Players.Count)\n                {\n                    ShowTunnelSelectionWindow((\"An error occured while contacting \" +\n                        \"the CnCNet tunnel server.\\nTry picking a different tunnel server:\").L10N(\"Client:Main:ConnectTunnelError1\"));\n                    AddNotice((\"An error occured while contacting the specified CnCNet \" +\n                        \"tunnel server. Please try using a different tunnel server\").L10N(\"Client:Main:ConnectTunnelError2\") + \" \", ERROR_MESSAGE_COLOR);\n                    return;\n                }\n\n                StringBuilder sb = new StringBuilder(\"START \");\n                sb.Append(UniqueGameID);\n                for (int pId = 0; pId < Players.Count; pId++)\n                {\n                    Players[pId].Port = playerPorts[pId];\n                    sb.Append(\";\");\n                    sb.Append(Players[pId].Name);\n                    sb.Append(\";\");\n                    sb.Append(tunnelHandler.CurrentTunnel.Address + \":\");\n                    sb.Append(playerPorts[pId]);\n                }\n                channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 10);\n            }\n            else\n            {\n                Logger.Log(\"One player MP -- starting!\");\n            }\n\n            cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), gameRoomName);\n\n            StartGame();\n        }\n\n        protected override void RequestPlayerOptions(int side, int color, int start, int team)\n        {\n            byte[] value = new byte[]\n            {\n                (byte)side,\n                (byte)color,\n                (byte)start,\n                (byte)team\n            };\n\n            int intValue = BinaryPrimitives.ReadInt32LittleEndian(value);\n\n            channel.SendCTCPMessage(\n                string.Format(\"OR {0}\", intValue),\n                QueuedMessageType.GAME_SETTINGS_MESSAGE, 6);\n        }\n\n        protected override void RequestReadyStatus()\n        {\n            if (Map == null || GameMode == null)\n            {\n                AddNotice((\"The game host needs to select a different map or \" +\n                    \"you will be unable to participate in the match.\").L10N(\"Client:Main:HostMustReplaceMap\"));\n\n                if (chkAutoReady.Checked)\n                    channel.SendCTCPMessage(\"R 0\", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5);\n\n                return;\n            }\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n            if (pInfo == null)\n                return;\n\n            int readyState = 0;\n\n            if (chkAutoReady.Checked)\n                readyState = 2;\n            else if (!pInfo.Ready)\n                readyState = 1;\n\n            channel.SendCTCPMessage($\"R {readyState}\", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5);\n        }\n\n        protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message));\n\n        /// <summary>\n        /// Handles player option requests received from non-host players.\n        /// </summary>\n        private void HandleOptionsRequest(string playerName, int options)\n        {\n            if (!IsHost)\n                return;\n\n            if (ProgramConstants.IsInGame)\n                return;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == playerName);\n\n            if (pInfo == null)\n                return;\n\n            byte[] bytes = new byte[sizeof(int)];\n            BinaryPrimitives.WriteInt32LittleEndian(bytes, options);\n\n            int side = bytes[0];\n            int color = bytes[1];\n            int start = bytes[2];\n            int team = bytes[3];\n\n            if (side < 0 || side > SideCount + RandomSelectorCount)\n                return;\n\n            if (color < 0 || color > MPColors.Count)\n                return;\n\n            // Disallowed sides from client, maps, or game modes do not take random selectors into account\n            // So, we need to insert \"false\" for each random at the beginning of this list AFTER getting them\n            // from client, maps, or game modes.\n            var randomDisallowedSides = new List<bool>(RandomSelectorCount);\n            for (int i = 0; i < RandomSelectorCount; i++)\n                randomDisallowedSides.Add(false);\n\n            var disallowedSides = randomDisallowedSides.Concat(GetDisallowedSides()).ToArray();\n\n            if (0 < side && side < SideCount && disallowedSides[side])\n                return;\n\n            if (GameModeMap?.CoopInfo != null)\n            {\n                if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount)\n                    return;\n\n                if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1))\n                    return;\n            }\n\n            if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true)))\n                return;\n\n            if (team < 0 || team > 4)\n                return;\n\n            if (side != pInfo.SideId\n                || start != pInfo.StartingLocation\n                || team != pInfo.TeamId)\n            {\n                ClearReadyStatuses();\n            }\n\n            pInfo.SideId = side;\n            pInfo.ColorId = color;\n            pInfo.StartingLocation = start;\n            pInfo.TeamId = team;\n\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n        }\n\n        /// <summary>\n        /// Handles \"I'm ready\" messages received from non-host players.\n        /// </summary>\n        private void HandleReadyRequest(string playerName, int readyStatus)\n        {\n            if (!IsHost)\n                return;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == playerName);\n\n            if (pInfo == null)\n                return;\n\n            pInfo.Ready = readyStatus > 0;\n            pInfo.AutoReady = readyStatus > 1;\n\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n        }\n\n        /// <summary>\n        /// Broadcasts player options to non-host players.\n        /// </summary>\n        protected override void BroadcastPlayerOptions()\n        {\n            // Broadcast player options\n            StringBuilder sb = new StringBuilder(\"PO \");\n            foreach (PlayerInfo pInfo in Players.Concat(AIPlayers))\n            {\n                if (pInfo.IsAI)\n                    sb.Append(pInfo.AILevel);\n                else\n                    sb.Append(pInfo.Name);\n                sb.Append(\";\");\n\n                // Combine the options into one integer to save bandwidth in\n                // cases where the player uses default options (this is common for AI players)\n                // Will hopefully make GameSurge kicking people a bit less common\n                byte[] byteArray = new byte[]\n                {\n                    (byte)pInfo.TeamId,\n                    (byte)pInfo.StartingLocation,\n                    (byte)pInfo.ColorId,\n                    (byte)pInfo.SideId,\n                };\n\n                int value = BinaryPrimitives.ReadInt32LittleEndian(byteArray);\n                sb.Append(value);\n                sb.Append(\";\");\n                if (!pInfo.IsAI)\n                {\n                    if (pInfo.AutoReady && !pInfo.IsInGame && !LastMapChangeWasInvalid)\n                        sb.Append(2);\n                    else\n                        sb.Append(Convert.ToInt32(pInfo.Ready));\n                    sb.Append(';');\n                }\n            }\n\n            channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11);\n        }\n\n        protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e)\n        {\n            base.PlayerExtraOptions_OptionsChanged(sender, e);\n            BroadcastPlayerExtraOptions();\n        }\n\n        protected override void BroadcastPlayerExtraOptions()\n        {\n            if (!IsHost)\n                return;\n\n            var playerExtraOptions = GetPlayerExtraOptions();\n\n            channel.SendCTCPMessage(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true);\n        }\n\n        /// <summary>\n        /// Handles player option messages received from the game host.\n        /// </summary>\n        private void ApplyPlayerOptions(string sender, string message)\n        {\n            if (sender != hostName)\n                return;\n\n            Players.Clear();\n            AIPlayers.Clear();\n\n            string[] parts = message.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);\n            for (int i = 0; i < parts.Length;)\n            {\n                PlayerInfo pInfo = new PlayerInfo();\n\n                string pName = parts[i];\n                int converted = Conversions.IntFromString(pName, -1);\n\n                if (converted > -1)\n                {\n                    pInfo.IsAI = true;\n                    pInfo.AILevel = converted;\n                    pInfo.Name = AILevelToName(converted);\n                }\n                else\n                {\n                    pInfo.Name = pName;\n\n                    // If we can't find the player from the channel user list,\n                    // ignore the player\n                    // They've either left the channel or got kicked before the\n                    // player options message reached us\n                    if (channel.Users.Find(pName) == null)\n                    {\n                        i += HUMAN_PLAYER_OPTIONS_LENGTH;\n                        continue;\n                    }\n                }\n\n                if (parts.Length <= i + 1)\n                    return;\n\n                int playerOptions = Conversions.IntFromString(parts[i + 1], -1);\n                if (playerOptions == -1)\n                    return;\n\n                byte[] byteArray = new byte[sizeof(int)];\n                BinaryPrimitives.WriteInt32LittleEndian(byteArray, playerOptions);\n\n                int team = byteArray[0];\n                int start = byteArray[1];\n                int color = byteArray[2];\n                int side = byteArray[3];\n\n                if (side < 0 || side > SideCount + RandomSelectorCount)\n                    return;\n\n                if (color < 0 || color > MPColors.Count)\n                    return;\n\n                if (start < 0 || start > MAX_PLAYER_COUNT)\n                    return;\n\n                if (team < 0 || team > 4)\n                    return;\n\n                pInfo.TeamId = byteArray[0];\n                pInfo.StartingLocation = byteArray[1];\n                pInfo.ColorId = byteArray[2];\n                pInfo.SideId = byteArray[3];\n\n                if (pInfo.IsAI)\n                {\n                    pInfo.Ready = true;\n                    AIPlayers.Add(pInfo);\n                    i += AI_PLAYER_OPTIONS_LENGTH;\n                }\n                else\n                {\n                    if (parts.Length <= i + 2)\n                        return;\n\n                    int readyStatus = Conversions.IntFromString(parts[i + 2], -1);\n\n                    if (readyStatus == -1)\n                        return;\n\n                    pInfo.Ready = readyStatus > 0;\n                    pInfo.AutoReady = readyStatus > 1;\n\n                    if (pInfo.Name == ProgramConstants.PLAYERNAME)\n                        btnLaunchGame.Text = pInfo.Ready ? BTN_LAUNCH_NOT_READY : BTN_LAUNCH_READY;\n\n                    Players.Add(pInfo);\n                    i += HUMAN_PLAYER_OPTIONS_LENGTH;\n                }\n            }\n\n            CopyPlayerDataToUI();\n        }\n\n        /// <summary>\n        /// Broadcasts game options to non-host players\n        /// when the host has changed an option.\n        /// </summary>\n        protected override void OnGameOptionChanged()\n        {\n            base.OnGameOptionChanged();\n\n            if (!IsHost)\n                return;\n\n            bool[] optionValues = new bool[CheckBoxes.Count];\n            for (int i = 0; i < CheckBoxes.Count; i++)\n                optionValues[i] = CheckBoxes[i].Checked;\n\n            // Let's pack the booleans into bytes\n            List<byte> byteList = Conversions.BoolArrayIntoBytes(optionValues).ToList();\n\n            while (byteList.Count % 4 != 0)\n                byteList.Add(0);\n\n            int integerCount = byteList.Count / 4;\n            byte[] byteArray = byteList.ToArray();\n\n            ExtendedStringBuilder sb = new ExtendedStringBuilder(\"GO \", true, ';');\n\n            for (int i = 0; i < integerCount; i++)\n                sb.Append(BinaryPrimitives.ReadInt32LittleEndian(byteArray.AsSpan(i * 4)));\n\n            // We don't gain much in most cases by packing the drop-down values\n            // (because they're bytes to begin with, and usually non-zero),\n            // so let's just transfer them as usual\n\n            foreach (GameLobbyDropDown dd in DropDowns)\n                sb.Append(dd.SelectedIndex);\n\n            sb.Append(Convert.ToInt32(Map?.Official ?? false));\n            sb.Append(Map?.SHA1 ?? string.Empty);\n            sb.Append(GameMode?.Name ?? string.Empty);\n            sb.Append(FrameSendRate);\n            sb.Append(MaxAhead);\n            sb.Append(ProtocolVersion);\n            sb.Append(RandomSeed);\n            sb.Append(Convert.ToInt32(RemoveStartingLocations));\n            sb.Append(Map?.UntranslatedName ?? string.Empty);\n\n            channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11);\n        }\n\n        /// <summary>\n        /// Handles game option messages received from the game host.\n        /// </summary>\n        private void ApplyGameOptions(string sender, string message)\n        {\n            if (sender != hostName)\n                return;\n\n            string[] parts = message.Split(';');\n\n            int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1;\n\n            int partIndex = checkBoxIntegerCount + DropDowns.Count;\n\n            if (parts.Length < partIndex + 6)\n            {\n                AddNotice((\"The game host has sent an invalid game options message! \" +\n                    \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostGameOptionInvalid\"), Color.Red);\n                return;\n            }\n\n            string mapOfficial = parts[partIndex];\n            bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true);\n\n            string mapSHA1 = parts[partIndex + 1];\n\n            string gameMode = parts[partIndex + 2];\n\n            int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate);\n            if (frameSendRate != FrameSendRate)\n            {\n                FrameSendRate = frameSendRate;\n                AddNotice(string.Format(\"The game host has changed FrameSendRate (order lag) to {0}\".L10N(\"Client:Main:HostChangeFrameSendRate\"), frameSendRate));\n            }\n\n            int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead);\n            if (maxAhead != MaxAhead)\n            {\n                MaxAhead = maxAhead;\n                AddNotice(string.Format(\"The game host has changed MaxAhead to {0}\".L10N(\"Client:Main:HostChangeMaxAhead\"), maxAhead));\n            }\n\n            int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion);\n            if (protocolVersion != ProtocolVersion)\n            {\n                ProtocolVersion = protocolVersion;\n                AddNotice(string.Format(\"The game host has changed ProtocolVersion to {0}\".L10N(\"Client:Main:HostChangeProtocolVersion\"), protocolVersion));\n            }\n\n            string mapName = parts[partIndex + 8];\n            GameModeMap currentGameModeMap = GameModeMap;\n\n            lastGameMode = gameMode;\n            lastMapSHA1 = mapSHA1;\n            lastMapName = mapName;\n\n            GameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1);\n            if (GameModeMap == null)\n            {\n                ChangeMap(null);\n\n                if (!string.IsNullOrEmpty(mapSHA1))\n                {\n                    if (!isMapOfficial)\n                        RequestMap(mapSHA1);\n                    else\n                        ShowOfficialMapMissingMessage(mapSHA1);\n                }\n            }\n            else if (GameModeMap != currentGameModeMap)\n            {\n                ChangeMap(GameModeMap);\n            }\n\n            // By changing the game options after changing the map, we know which\n            // game options were changed by the map and which were changed by the game host\n\n            // If the map doesn't exist on the local installation, it's impossible\n            // to know which options were set by the host and which were set by the\n            // map, so we'll just assume that the host has set all the options.\n            // Very few (if any) custom maps force options, so it'll be correct nearly always\n\n            for (int i = 0; i < checkBoxIntegerCount; i++)\n            {\n                if (parts.Length <= i)\n                    return;\n\n                int checkBoxStatusInt;\n                bool success = int.TryParse(parts[i], out checkBoxStatusInt);\n\n                if (!success)\n                {\n                    AddNotice((\"Failed to parse check box options sent by game host!\" +\n                        \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostCheckBoxParseError\"), Color.Red);\n                    return;\n                }\n\n                byte[] byteArray = new byte[sizeof(int)];\n                BinaryPrimitives.WriteInt32LittleEndian(byteArray, checkBoxStatusInt);\n                bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray);\n\n                for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++)\n                {\n                    int gameOptionIndex = i * 32 + optionIndex;\n\n                    if (gameOptionIndex >= CheckBoxes.Count)\n                        break;\n\n                    GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex];\n\n                    if (checkBox.Checked != boolArray[optionIndex])\n                    {\n                        if (boolArray[optionIndex])\n                            AddNotice(string.Format(\"The game host has enabled {0}\".L10N(\"Client:Main:HostEnableOption\"), checkBox.Text));\n                        else\n                            AddNotice(string.Format(\"The game host has disabled {0}\".L10N(\"Client:Main:HostDisableOption\"), checkBox.Text));\n                    }\n\n                    CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex];\n                }\n            }\n\n            for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++)\n            {\n                if (parts.Length <= i)\n                {\n                    AddNotice((\"The game host has sent an invalid game options message! \" +\n                    \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostGameOptionInvalid\"), Color.Red);\n                    return;\n                }\n\n                int ddSelectedIndex;\n                bool success = int.TryParse(parts[i], out ddSelectedIndex);\n\n                if (!success)\n                {\n                    AddNotice((\"Failed to parse drop down options sent by game host (2)! \" +\n                        \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostDropDownParseError\"), Color.Red);\n                    return;\n                }\n\n                GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount];\n\n                if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count)\n                    continue;\n\n                if (dd.SelectedIndex != ddSelectedIndex)\n                {\n                    string ddName = dd.OptionName;\n                    if (dd.OptionName == null)\n                        ddName = dd.Name;\n\n                    AddNotice(string.Format(\"The game host has set {0} to {1}\".L10N(\"Client:Main:HostSetOption\"), ddName, dd.Items[ddSelectedIndex].Text));\n                }\n\n                DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex;\n            }\n\n            int randomSeed;\n            bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed);\n\n            if (!parseSuccess)\n            {\n                AddNotice((\"Failed to parse random seed from game options message! \" +\n                    \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostRandomSeedError\"), Color.Red);\n            }\n\n            bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7],\n                Convert.ToInt32(RemoveStartingLocations)));\n            SetRandomStartingLocations(removeStartingLocations);\n\n            RandomSeed = randomSeed;\n        }\n\n        private void RequestMap(string mapSHA1)\n        {\n            if (UserINISettings.Instance.EnableMapSharing)\n            {\n                AddNotice(\"The game host has selected a map that doesn't exist on your installation.\".L10N(\"Client:Main:MapNotExist\"));\n                mapSharingConfirmationPanel.ShowForMapDownload();\n            }\n            else\n            {\n                AddNotice(\"The game host has selected a map that doesn't exist on your installation.\".L10N(\"Client:Main:MapNotExist\") + \" \" +\n                    (\"Because you've disabled map sharing, it cannot be transferred. The game host needs \" +\n                    \"to change the map or you will be unable to participate in the match.\").L10N(\"Client:Main:MapSharingDisabledNotice\"));\n                channel.SendCTCPMessage(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9);\n            }\n        }\n\n        private void ShowOfficialMapMissingMessage(string sha1)\n        {\n            AddNotice((\"The game host has selected an official map that doesn't exist on your installation. \" +\n                \"This could mean that the game host has modified game files, or is running a different game version. \" +\n                \"They need to change the map or you will be unable to participate in the match.\").L10N(\"Client:Main:OfficialMapNotExist\"));\n            channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + \" \" + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9);\n        }\n\n        private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e)\n        {\n            Logger.Log(\"Map sharing confirmed.\");\n            AddNotice(\"Attempting to download map.\".L10N(\"Client:Main:DownloadingMap\"));\n            mapSharingConfirmationPanel.SetDownloadingStatus();\n            MapSharer.DownloadMap(lastMapSHA1, localGame, lastMapName);\n        }\n\n        protected override void ChangeMap(GameModeMap gameModeMap)\n        {\n            mapSharingConfirmationPanel.Disable();\n            base.ChangeMap(gameModeMap);\n        }\n\n        protected override void HandleMapUpdated(Map updatedMap, string previousSHA1)\n        {\n            base.HandleMapUpdated(updatedMap, previousSHA1);\n\n            // If the host's currently selected map was updated, broadcast the new map to other players\n            if (IsHost && Map != null && Map.SHA1 == updatedMap.SHA1)\n                OnGameOptionChanged();\n        }\n\n        /// <summary>\n        /// Signals other players that the local player has returned from the game,\n        /// and unlocks the game as well as generates a new random seed as the game host.\n        /// </summary>\n        protected override void GameProcessExited()\n        {\n            ResetGameState();\n        }\n\n        protected void GameStartAborted()\n        {\n            ResetGameState();\n        }\n\n        protected void ResetGameState() \n        {\n            base.GameProcessExited();\n\n            channel.SendCTCPMessage(\"RETURN\", QueuedMessageType.SYSTEM_MESSAGE, 20);\n            ReturnNotification(ProgramConstants.PLAYERNAME);\n\n            if (IsHost)\n            {\n                RandomSeed = random.Next();\n                OnGameOptionChanged();\n                ClearReadyStatuses();\n                CopyPlayerDataToUI();\n                BroadcastPlayerOptions();\n                BroadcastPlayerExtraOptions();\n                StartInactiveCheck();\n\n                if (Players.Count < playerLimit)\n                    UnlockGame(true);\n            }\n        }\n\n        /// <summary>\n        /// Handles the \"START\" (game start) command sent by the game host.\n        /// </summary>\n        private void NonHostLaunchGame(string sender, string message)\n        {\n            if (sender != hostName)\n                return;\n\n            if (Map == null)\n            {\n                GameStartAborted();\n                return;\n            }\n\n            string[] parts = message.Split(';');\n\n            if (parts.Length < 1)\n                return;\n\n            UniqueGameID = Conversions.IntFromString(parts[0], -1);\n            if (UniqueGameID < 0)\n                return;\n\n            var recentPlayers = new List<string>();\n\n            for (int i = 1; i < parts.Length; i += 2)\n            {\n                if (parts.Length <= i + 1)\n                    return;\n\n                string pName = parts[i];\n                string[] ipAndPort = parts[i + 1].Split(':');\n\n                if (ipAndPort.Length < 2)\n                    return;\n\n                int port;\n                bool success = int.TryParse(ipAndPort[1], out port);\n\n                if (!success)\n                    return;\n\n                if (pName == ProgramConstants.PLAYERNAME)\n                {\n                    var matchedTunnel = tunnelHandler.Tunnels\n                        .FirstOrDefault(t =>\n                            string.Equals(t.Address, ipAndPort[0], StringComparison.OrdinalIgnoreCase));\n\n                    if (matchedTunnel != null)\n                    {\n                        tunnelHandler.CurrentTunnel = matchedTunnel;\n                    }\n                    else\n                    {\n                        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\"));\n                        Logger.Log(\"Failed to match tunnel address: \" + ipAndPort[0]);\n                        return;\n                    }\n                }\n\n                PlayerInfo pInfo = Players.Find(p => p.Name == pName);\n\n                if (pInfo == null)\n                    return;\n\n                pInfo.Port = port;\n                recentPlayers.Add(pName);\n            }\n            cncnetUserData.AddRecentPlayers(recentPlayers, gameRoomName);\n\n            StartGame();\n        }\n\n        protected override void StartGame()\n        {\n            AddNotice(\"Starting game...\".L10N(\"Client:Main:StartingGame\"));\n\n            FileHashCalculator fhc = new FileHashCalculator();\n            fhc.CalculateHashes();\n\n            if (gameFilesHash != fhc.GetCompleteHash())\n            {\n                Logger.Log(\"Game files modified during client session!\");\n                channel.SendCTCPMessage(CHEAT_DETECTED_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0);\n                HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME);\n            }\n\n            StopInactiveCheck();\n            channel.SendCTCPMessage(\"STRTD\", QueuedMessageType.SYSTEM_MESSAGE, 20);\n\n            base.StartGame();\n        }\n\n        protected override void WriteSpawnIniAdditions(IniFile iniFile)\n        {\n            base.WriteSpawnIniAdditions(iniFile);\n\n            iniFile.SetStringValue(\"Tunnel\", \"Ip\", tunnelHandler.CurrentTunnel.Address);\n            iniFile.SetIntValue(\"Tunnel\", \"Port\", tunnelHandler.CurrentTunnel.Port);\n\n            iniFile.SetIntValue(\"Settings\", \"GameID\", UniqueGameID);\n            iniFile.SetBooleanValue(\"Settings\", \"Host\", IsHost);\n\n            PlayerInfo localPlayer = FindLocalPlayer();\n\n            if (localPlayer == null)\n                return;\n\n            iniFile.SetIntValue(\"Settings\", \"Port\", localPlayer.Port);\n        }\n\n        protected override void SendChatMessage(string message) => channel.SendChatMessage(message, chatColor);\n\n        #region Notifications\n\n        private void HandleNotification(string sender, Action handler)\n        {\n            if (sender != hostName)\n                return;\n\n            handler();\n        }\n\n        private void HandleIntNotification(string sender, int parameter, Action<int> handler)\n        {\n            if (sender != hostName)\n                return;\n\n            handler(parameter);\n        }\n\n        protected override void GetReadyNotification()\n        {\n            base.GetReadyNotification();\n#if WINFORMS\n            WindowManager.FlashWindow();\n#endif\n            TopBar.SwitchToPrimary();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"GETREADY\", QueuedMessageType.GAME_GET_READY_MESSAGE, 0);\n        }\n\n        protected override void AISpectatorsNotification()\n        {\n            base.AISpectatorsNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"AISPECS\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void InsufficientPlayersNotification()\n        {\n            base.InsufficientPlayersNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"INSFSPLRS\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void TooManyPlayersNotification()\n        {\n            base.TooManyPlayersNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"TMPLRS\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void SharedColorsNotification()\n        {\n            base.SharedColorsNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"CLRS\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void SharedStartingLocationNotification()\n        {\n            base.SharedStartingLocationNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"SLOC\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void LockGameNotification()\n        {\n            base.LockGameNotification();\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"LCKGME\", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void NotVerifiedNotification(int playerIndex)\n        {\n            base.NotVerifiedNotification(playerIndex);\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"NVRFY \" + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        protected override void StillInGameNotification(int playerIndex)\n        {\n            base.StillInGameNotification(playerIndex);\n\n            if (IsHost)\n                channel.SendCTCPMessage(\"INGM \" + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0);\n        }\n\n        private void GameStartedNotification(string sender)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo != null)\n                pInfo.IsInGame = true;\n\n            CopyPlayerDataToUI();\n        }\n\n        private void ReturnNotification(string sender)\n        {\n            AddNotice(string.Format(\"{0} has returned from the game.\".L10N(\"Client:Main:PlayerReturned\"), sender));\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo != null)\n                pInfo.IsInGame = false;\n\n            sndReturnSound.Play();\n            CopyPlayerDataToUI();\n        }\n\n        private void HandleTunnelPing(string sender, int ping)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender));\n            if (pInfo != null)\n            {\n                pInfo.Ping = ping;\n                UpdatePlayerPingIndicator(pInfo);\n            }\n        }\n\n        private void FileHashNotification(string sender, string filesHash)\n        {\n            if (!IsHost)\n                return;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo != null)\n                pInfo.HashReceived = true;\n            CopyPlayerDataToUI();\n\n            if (filesHash != gameFilesHash)\n            {\n                channel.SendCTCPMessage(\"MM \" + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10);\n                CheaterNotification(ProgramConstants.PLAYERNAME, sender);\n            }\n        }\n\n        private void CheaterNotification(string sender, string cheaterName)\n        {\n            if (sender != hostName)\n                return;\n\n            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);\n        }\n\n        protected override void BroadcastDiceRoll(int dieSides, int[] results)\n        {\n            string resultString = string.Join(\",\", results);\n            channel.SendCTCPMessage($\"{DICE_ROLL_MESSAGE} {dieSides},{resultString}\", QueuedMessageType.CHAT_MESSAGE, 0);\n            PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results);\n        }\n\n        #endregion\n\n        protected override void HandleLockGameButtonClick()\n        {\n            if (!Locked)\n            {\n                AddNotice(\"You've locked the game room.\".L10N(\"Client:Main:RoomLockedByYou\"));\n                LockGame();\n            }\n            else\n            {\n                if (Players.Count < playerLimit)\n                {\n                    AddNotice(\"You've unlocked the game room.\".L10N(\"Client:Main:RoomUnlockedByYou\"));\n                    UnlockGame(false);\n                }\n                else\n                    AddNotice(string.Format(\n                        \"Cannot unlock game; the player limit ({0}) has been reached.\".L10N(\"Client:Main:RoomCantUnlockAsLimit\"), playerLimit));\n            }\n        }\n\n        protected override void LockGame()\n        {\n            connectionManager.SendCustomMessage(new QueuedMessage(\n                string.Format(\"MODE {0} +i\", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1));\n\n            Locked = true;\n            btnLockGame.Text = \"Unlock Game\".L10N(\"Client:Main:UnlockGame\");\n            AccelerateGameBroadcasting();\n        }\n\n        protected override void UnlockGame(bool announce)\n        {\n            connectionManager.SendCustomMessage(new QueuedMessage(\n                string.Format(\"MODE {0} -i\", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1));\n\n            Locked = false;\n            if (announce)\n                AddNotice(\"The game room has been unlocked.\".L10N(\"Client:Main:GameRoomUnlocked\"));\n            btnLockGame.Text = \"Lock Game\".L10N(\"Client:Main:LockGame\");\n            AccelerateGameBroadcasting();\n        }\n\n        protected override void KickPlayer(int playerIndex)\n        {\n            if (playerIndex >= Players.Count)\n                return;\n\n            var pInfo = Players[playerIndex];\n\n            AddNotice(string.Format(\"Kicking {0} from the game...\".L10N(\"Client:Main:KickPlayer\"), pInfo.Name));\n            channel.SendKickMessage(pInfo.Name, 8);\n        }\n\n        protected override void BanPlayer(int playerIndex)\n        {\n            if (playerIndex >= Players.Count)\n                return;\n\n            var pInfo = Players[playerIndex];\n\n            var user = connectionManager.UserList.Find(u => u.Name == pInfo.Name);\n\n            if (user != null)\n            {\n                AddNotice(string.Format(\"Banning and kicking {0} from the game...\".L10N(\"Client:Main:BanAndKickPlayer\"), pInfo.Name));\n                channel.SendBanMessage(user.Hostname, 8);\n                channel.SendKickMessage(user.Name, 8);\n            }\n        }\n\n        private void HandleCheatDetectedMessage(string sender) =>\n            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);\n\n        private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort)\n        {\n            if (sender != hostName)\n                return;\n\n            string[] split = tunnelAddressAndPort.Split(':');\n            string tunnelAddress = split[0];\n            int tunnelPort = int.Parse(split[1]);\n\n            CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort);\n            if (tunnel == null)\n            {\n                tunnelErrorMode = true;\n                AddNotice((\"The game host has selected an invalid tunnel server! \" +\n                    \"The game host needs to change the server or you will be unable \" +\n                    \"to participate in the match.\").L10N(\"Client:Main:HostInvalidTunnel\"),\n                    Color.Yellow);\n                UpdateLaunchGameButtonStatus();\n                return;\n            }\n\n            tunnelErrorMode = false;\n            HandleTunnelServerChange(tunnel);\n            UpdateLaunchGameButtonStatus();\n        }\n\n        /// <summary>\n        /// Changes the tunnel server used for the game.\n        /// </summary>\n        /// <param name=\"tunnel\">The new tunnel server to use.</param>\n        private void HandleTunnelServerChange(CnCNetTunnel tunnel)\n        {\n            tunnelHandler.CurrentTunnel = tunnel;\n            AddNotice(string.Format(\"The game host has changed the tunnel server to: {0}\".L10N(\"Client:Main:HostChangeTunnel\"), tunnel.Name));\n\n            foreach (PlayerInfo pInfo in Players)\n            {\n                pInfo.Ping = -1;\n                UpdatePlayerPingIndicator(pInfo);\n            }\n\n            CopyPlayerDataToUI();\n            UpdatePing();\n        }\n\n        protected override bool UpdateLaunchGameButtonStatus()\n        {\n            btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !tunnelErrorMode;\n            return btnLaunchGame.Enabled;\n        }\n\n        #region CnCNet map sharing\n\n        private void MapSharer_MapDownloadFailed(object sender, SHA1EventArgs e)\n            => WindowManager.AddCallback(new Action<SHA1EventArgs>(MapSharer_HandleMapDownloadFailed), e);\n\n        private void MapSharer_HandleMapDownloadFailed(SHA1EventArgs e)\n        {\n            // If the host has already uploaded the map, we shouldn't request them to re-upload it\n            if (hostUploadedMaps.Contains(e.SHA1))\n            {\n                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\"));\n                mapSharingConfirmationPanel.SetFailedStatus();\n\n                channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + \" \" + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);\n                return;\n            }\n            else if (chatCommandDownloadedMaps.Contains(e.SHA1))\n            {\n                // Notify the user that their chat command map download failed.\n                // Do not notify other users with a CTCP message as this is irrelevant to them.\n                AddNotice(\"Downloading map via chat command has failed. Check the map ID and try again.\".L10N(\"Client:Main:DownloadMapCommandFailedGeneric\"));\n                mapSharingConfirmationPanel.SetFailedStatus();\n                return;\n            }\n\n            AddNotice(\"Requesting the game host to upload the map to the CnCNet map database.\".L10N(\"Client:Main:RequestHostUploadMapToDB\"));\n\n            channel.SendCTCPMessage(MAP_SHARING_UPLOAD_REQUEST + \" \" + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);\n        }\n\n        private void MapSharer_MapDownloadComplete(object sender, SHA1EventArgs e) =>\n            WindowManager.AddCallback(new Action<SHA1EventArgs>(MapSharer_HandleMapDownloadComplete), e);\n\n        private void MapSharer_HandleMapDownloadComplete(SHA1EventArgs e)\n        {\n            string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName);\n            Logger.Log(\"Map \" + mapFileName + \" downloaded successfully.\");\n\n            // MapLoader_MapChanged will fire when it's processed.\n        }\n\n        private void MapLoader_MapChanged(object sender, MapChangedEventArgs e)\n        {\n            if (e.ChangeType != MapChangeType.Added)\n                return;\n\n            bool isFromChatCommand = chatCommandDownloadedMaps.Contains(e.Map.SHA1);\n            bool isFromHostSharing = lastMapSHA1 == e.Map.SHA1 && !isFromChatCommand;\n\n            if (!isFromChatCommand && !isFromHostSharing)\n                return;\n\n            AddNotice($\"Map {e.Map.Name} loaded successfully.\");\n\n            GameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == e.Map.SHA1);\n            ChangeMap(GameModeMap);\n\n            if (isFromChatCommand)\n                chatCommandDownloadedMaps.Remove(e.Map.SHA1);\n        }\n\n        protected override void HandleMapAdded(Map addedMap)\n        {\n            bool isFromChatCommand = chatCommandDownloadedMaps.Contains(addedMap.SHA1);\n            bool isFromHostSharing = lastMapSHA1 == addedMap.SHA1 && !isFromChatCommand;\n\n            // If this is a map we downloaded, select it\n            if (isFromChatCommand || isFromHostSharing)\n            {\n                AddNotice($\"Map {addedMap.Name} loaded successfully.\");\n\n                RefreshGameModeFilter();\n\n                GameModeMap gameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == addedMap.SHA1);\n\n                if (gameModeMap != null)\n                {\n                    // select game mode\n                    int gameModeIndex = ddGameModeMapFilter.Items.FindIndex(item =>\n                        (item.Tag as GameModeMapFilter)?.GetGameModeMaps().Any(gmm => gmm.GameMode.Name == gameModeMap.GameMode.Name) ?? false);\n\n                    if (gameModeIndex >= 0)\n                        ddGameModeMapFilter.SelectedIndex = gameModeIndex;\n\n                    ListMaps();\n\n                    // select map\n                    for (int i = 0; i < lbGameModeMapList.ItemCount; i++)\n                    {\n                        var item = lbGameModeMapList.GetItem(1, i);\n                        if ((item.Tag as GameModeMap)?.Map.SHA1 == addedMap.SHA1)\n                        {\n                            lbGameModeMapList.SelectedIndex = i;\n                            break;\n                        }\n                    }\n\n                    ChangeMap(gameModeMap);\n                }\n\n                if (isFromChatCommand)\n                    chatCommandDownloadedMaps.Remove(addedMap.SHA1);\n            }\n            else\n            {\n                base.HandleMapAdded(addedMap);\n            }\n        }\n\n        private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) =>\n            WindowManager.AddCallback(new Action<MapEventArgs>(MapSharer_HandleMapUploadFailed), e);\n\n        private void MapSharer_HandleMapUploadFailed(MapEventArgs e)\n        {\n            Map map = e.Map;\n\n            hostUploadedMaps.Add(map.SHA1);\n\n            AddNotice(string.Format(\"Uploading map {0} to the CnCNet map database failed.\".L10N(\"Client:Main:UpdateMapToDBFailed\"), map.Name));\n            if (map == Map)\n            {\n                AddNotice(\"You need to change the map or some players won't be able to participate in this match.\".L10N(\"Client:Main:YouMustReplaceMap\"));\n                channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + \" \" + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);\n            }\n        }\n\n        private void MapSharer_MapUploadComplete(object sender, MapEventArgs e) =>\n            WindowManager.AddCallback(new Action<MapEventArgs>(MapSharer_HandleMapUploadComplete), e);\n\n        private void MapSharer_HandleMapUploadComplete(MapEventArgs e)\n        {\n            hostUploadedMaps.Add(e.Map.SHA1);\n\n            AddNotice(string.Format(\"Uploading map {0} to the CnCNet map database complete.\".L10N(\"Client:Main:UpdateMapToDBSuccess\"), e.Map.Name));\n            if (e.Map == Map)\n            {\n                channel.SendCTCPMessage(MAP_SHARING_DOWNLOAD_REQUEST + \" \" + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);\n            }\n        }\n\n        /// <summary>\n        /// Handles a map upload request sent by a player.\n        /// </summary>\n        /// <param name=\"sender\">The sender of the request.</param>\n        /// <param name=\"mapSHA1\">The SHA1 of the requested map.</param>\n        private void HandleMapUploadRequest(string sender, string mapSHA1)\n        {\n            if (hostUploadedMaps.Contains(mapSHA1))\n            {\n                Logger.Log(\"HandleMapUploadRequest: Map \" + mapSHA1 + \" is already uploaded!\");\n                return;\n            }\n\n            Map map = null;\n\n            foreach (GameMode gm in GameModeMaps.GameModes)\n            {\n                map = gm.Maps.Find(m => m.SHA1 == mapSHA1);\n\n                if (map != null)\n                    break;\n            }\n\n            if (map == null)\n            {\n                Logger.Log(\"Unknown map upload request from \" + sender + \": \" + mapSHA1);\n                return;\n            }\n\n            if (map.Official)\n            {\n                Logger.Log(\"HandleMapUploadRequest: Map is official, so skip request\");\n\n                AddNotice(string.Format((\"{0} doesn't have the map '{1}' on their local installation. \" +\n                    \"The map needs to be changed or {0} is unable to participate in the match.\").L10N(\"Client:Main:PlayerMissingMap\"),\n                    sender, map.Name));\n\n                return;\n            }\n\n            if (!IsHost)\n                return;\n\n            AddNotice(string.Format((\"{0} doesn't have the map '{1}' on their local installation. \" +\n                \"Attempting to upload the map to the CnCNet map database.\").L10N(\"Client:Main:UpdateMapToDBPrompt\"),\n                sender, map.Name));\n\n            MapSharer.UploadMap(map, localGame);\n        }\n\n        /// <summary>\n        /// Handles a map transfer failure message sent by either the player or the game host.\n        /// </summary>\n        private void HandleMapTransferFailMessage(string sender, string sha1)\n        {\n            if (sender == hostName)\n            {\n                AddNotice(\"The game host failed to upload the map to the CnCNet map database.\".L10N(\"Client:Main:HostUpdateMapToDBFailed\"));\n\n                hostUploadedMaps.Add(sha1);\n\n                if (lastMapSHA1 == sha1 && Map == null)\n                {\n                    AddNotice(\"The game host needs to change the map or you won't be able to participate in this match.\".L10N(\"Client:Main:HostMustChangeMap\"));\n                }\n\n                return;\n            }\n\n            if (lastMapSHA1 == sha1)\n            {\n                if (!IsHost)\n                {\n                    AddNotice(string.Format(\"{0} has failed to download the map from the CnCNet map database.\".L10N(\"Client:Main:PlayerDownloadMapFailed\") + \" \" +\n                        \"The host needs to change the map or {0} won't be able to participate in this match.\".L10N(\"Client:Main:HostNeedChangeMapForPlayer\"), sender));\n                }\n                else\n                {\n                    AddNotice(string.Format(\"{0} has failed to download the map from the CnCNet map database.\".L10N(\"Client:Main:PlayerDownloadMapFailed\") + \" \" +\n                        \"You need to change the map or {0} won't be able to participate in this match.\".L10N(\"Client:Main:YouNeedChangeMapForPlayer\"), sender));\n                }\n            }\n        }\n\n        private void HandleMapDownloadRequest(string sender, string sha1)\n        {\n            if (sender != hostName)\n                return;\n\n            hostUploadedMaps.Add(sha1);\n\n            if (lastMapSHA1 == sha1 && Map == null)\n            {\n                Logger.Log(\"The game host has uploaded the map into the database. Re-attempting download...\");\n                MapSharer.DownloadMap(sha1, localGame, lastMapName);\n            }\n        }\n\n        private void HandleMapSharingBlockedMessage(string sender)\n        {\n            AddNotice(string.Format((\"The selected map doesn't exist on {0}'s installation, and they \" +\n                \"have map sharing disabled in settings. The game host needs to change to a non-custom map or \" +\n                \"they will be unable to participate in this match.\").L10N(\"Client:Main:PlayerMissingMapDisabledSharing\"), sender));\n        }\n\n        /// <summary>\n        /// Download a map from CNCNet using a map hash ID.\n        ///\n        /// Users and testers can get map hash IDs from this URL template:\n        ///\n        /// - http://mapdb.cncnet.org/search.php?game=GAME_ID&search=MAP_NAME_SEARCH_STRING\n        ///\n        /// </summary>\n        /// <param name=\"parameters\">\n        /// 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.\n        /// Every character after the first space will be treated as part of the map name.\n        ///\n        /// \"?\" characters are removed from the sha1 due to weird copy and paste behavior from the map search endpoint.\n        /// </param>\n        private void DownloadMapByIdCommand(string parameters)\n        {\n            string sha1;\n            string mapName;\n            string message;\n\n            // Make sure no spaces at the beginning or end of the string will mess up arg parsing.\n            parameters = parameters.Trim();\n            // Check if the parameter's contain spaces.\n            // The presence of spaces indicates a user-specified map name.\n            int firstSpaceIndex = parameters.IndexOf(' ');\n\n            if (firstSpaceIndex == -1)\n            {\n                // The user did not supply a map name.\n                sha1 = parameters;\n                mapName = \"user_chat_command_download\";\n            }\n            else\n            {\n                // User supplied a map name.\n                sha1 = parameters.Substring(0, firstSpaceIndex);\n                mapName = parameters.Substring(firstSpaceIndex + 1);\n                mapName = mapName.Trim();\n            }\n\n            // Remove erroneous \"?\". These sneak in when someone double-clicks a map ID and copies it from the cncnet search endpoint.\n            // 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.\n            sha1 = sha1.Replace(\"?\", \"\");\n\n            // See if the user already has this map, with any filename, before attempting to download it.\n            GameModeMap loadedMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == sha1);\n\n            if (loadedMap != null)\n            {\n                message = String.Format(\n                    \"The map for ID \\\"{0}\\\" is already loaded from \\\"{1}.{2}\\\", delete the existing file before trying again.\".L10N(\"Client:Main:DownloadMapCommandSha1AlreadyExists\"),\n                    sha1,\n                    loadedMap.Map.BaseFilePath,\n                    ClientConfiguration.Instance.MapFileExtension);\n                AddNotice(message, Color.Yellow);\n                Logger.Log(message);\n                return;\n            }\n\n            // Replace any characters that are not safe for filenames.\n            char replaceUnsafeCharactersWith = '-';\n            // Use a hashset instead of an array for quick lookups in `invalidChars.Contains()`.\n            HashSet<char> invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());\n            string safeMapName = new String(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray());\n\n            chatCommandDownloadedMaps.Add(sha1);\n\n            message = String.Format(\"Attempting to download map via chat command: sha1={0}, mapName={1}\".L10N(\"Client:Main:DownloadMapCommandStartingDownload\"), sha1, mapName);\n            Logger.Log(message);\n            AddNotice(message);\n\n            MapSharer.DownloadMap(sha1, localGame, safeMapName);\n        }\n\n        #endregion\n\n        #region Game broadcasting logic\n\n        /// <summary>\n        /// Lowers the time until the next game broadcasting message.\n        /// </summary>\n        private void AccelerateGameBroadcasting() =>\n            gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION));\n\n        private void BroadcastGame()\n        {\n            Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame));\n\n            if (broadcastChannel == null)\n                return;\n\n            if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500)\n                return;\n\n            StringBuilder sb = new StringBuilder(\"GAME \");\n            sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION);\n            sb.Append(\";\");\n            sb.Append(ProgramConstants.GAME_VERSION);\n            sb.Append(\";\");\n            sb.Append(playerLimit);\n            sb.Append(\";\");\n            sb.Append(channel.ChannelName);\n            sb.Append(\";\");\n            sb.Append(gameRoomName);\n            sb.Append(\";\");\n            if (Locked)\n                sb.Append(\"1\");\n            else\n                sb.Append(\"0\");\n            sb.Append(Convert.ToInt32(isCustomPassword));\n            sb.Append(Convert.ToInt32(closed));\n            sb.Append(\"0\"); // IsLoadedGame\n            sb.Append(\"0\"); // IsLadder\n            sb.Append(\";\");\n            foreach (PlayerInfo pInfo in Players)\n            {\n                sb.Append(pInfo.Name);\n                sb.Append(\",\");\n            }\n\n            sb.Remove(sb.Length - 1, 1);\n            sb.Append(\";\");\n            sb.Append(Map?.UntranslatedName ?? string.Empty);\n            sb.Append(\";\");\n            sb.Append(GameMode?.UntranslatedUIName ?? string.Empty);\n            sb.Append(\";\");\n            sb.Append(tunnelHandler.CurrentTunnel.Address + \":\" + tunnelHandler.CurrentTunnel.Port);\n            sb.Append(\";\");\n            sb.Append(0); // LoadedGameId\n            sb.Append(\";\");\n            sb.Append(skillLevel); // SkillLevel\n            sb.Append(\";\");\n            sb.Append(Map?.SHA1);\n\n            List<IGameSessionSetting> broadcastableSettings = GetBroadcastableSettings();\n\n            List<int> gameOptionValues = new();\n\n            int checkboxCount = CheckBoxes.Count(cb => cb.BroadcastToLobby);\n            if (checkboxCount > 0)\n            {\n                bool[] checkboxValues = new bool[checkboxCount];\n                for (int i = 0; i < checkboxCount; i++)\n                    checkboxValues[i] = CheckBoxes.Where(cb => cb.BroadcastToLobby).ElementAt(i).Checked;\n\n                List<byte> byteList = Conversions.BoolArrayIntoBytes(checkboxValues).ToList();\n\n                // Pad to multiple of 4 bytes\n                while (byteList.Count % 4 != 0)\n                    byteList.Add(0);\n\n                byte[] byteArray = byteList.ToArray();\n\n                // Convert bytes to integers\n                for (int i = 0; i < byteArray.Length / 4; i++)\n                    gameOptionValues.Add(BinaryPrimitives.ReadInt32LittleEndian(byteArray.AsSpan(i * 4)));\n            }\n\n            // Add dropdown indices\n            int dropdownCount = DropDowns.Count(dd => dd.BroadcastToLobby);\n            if (dropdownCount > 0)\n                gameOptionValues.AddRange(DropDowns.Where(dd => dd.BroadcastToLobby).Select(dd => dd.SelectedIndex));\n\n            sb.Append(\";\");\n            if (gameOptionValues.Count > 0)\n                sb.Append(string.Join(\",\", gameOptionValues));\n\n            broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20);\n        }\n\n        #endregion\n\n        public override string GetSwitchName() => \"Game Lobby\".L10N(\"Client:Main:GameLobby\");\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/CommandHandlerBase.cs",
    "content": "﻿namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    public abstract class CommandHandlerBase\n    {\n        public CommandHandlerBase(string commandName)\n        {\n            CommandName = commandName;\n        }\n\n        public string CommandName { get; private set; }\n\n        public abstract bool Handle(string sender, string message);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    public class IntCommandHandler : CommandHandlerBase\n    {\n        public IntCommandHandler(string commandName, Action<string, int> handler) : base(commandName)\n        {\n            this.handler = handler;\n        }\n\n        Action<string, int> handler;\n\n        public override bool Handle(string sender, string message)\n        {\n            if (message.Length < CommandName.Length + 1)\n                return false;\n\n            if (message.StartsWith(CommandName))\n            {\n                int value;\n                bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value);\n\n                if (success)\n                {\n                    handler(sender, value);\n                    return true;\n                }\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    public class IntNotificationHandler : CommandHandlerBase\n    {\n        public IntNotificationHandler(string commandName, Action<string, int, Action<int>> action,\n            Action<int> innerAction) : base(commandName)\n        {\n            this.action = action;\n            this.innerAction = innerAction;\n        }\n\n        Action<string, int, Action<int>> action;\n        Action<int> innerAction;\n\n        public override bool Handle(string sender, string message)\n        {\n            if (message.StartsWith(CommandName))\n            {\n                string intPart = message.Substring(CommandName.Length + 1);\n                int value;\n                bool success = int.TryParse(intPart, out value);\n\n                action(sender, value, innerAction);\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/NoParamCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    /// <summary>\n    /// A command handler that handles a command that has no parameter aside from the sender.\n    /// </summary>\n    public class NoParamCommandHandler : CommandHandlerBase\n    {\n        public NoParamCommandHandler(string commandName, Action<string> commandHandler) : base(commandName)\n        {\n            this.commandHandler = commandHandler;\n        }\n\n        Action<string> commandHandler;\n\n        public override bool Handle(string sender, string message)\n        {\n            if (message == CommandName)\n            {\n                commandHandler(sender);\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/NotificationHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    public class NotificationHandler : CommandHandlerBase\n    {\n        public NotificationHandler(string commandName, Action<string, Action> action,\n            Action innerAction) : base(commandName)\n        {\n            this.action = action;\n            this.innerAction = innerAction;\n        }\n\n        Action<string, Action> action;\n        Action innerAction;\n\n        public override bool Handle(string sender, string message)\n        {\n            if (message == CommandName)\n            {\n                action(sender, innerAction);\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers\n{\n    class StringCommandHandler : CommandHandlerBase\n    {\n        public StringCommandHandler(string commandName, Action<string, string> commandHandler) : base(commandName)\n        {\n            this.commandHandler = commandHandler;\n        }\n\n        private Action<string, string> commandHandler;\n\n        public override bool Handle(string sender, string message)\n        {\n            if (message.Length < CommandName.Length + 1)\n                return false;\n\n            if (message.StartsWith(CommandName))\n            {\n                string parameters = message.Substring(CommandName.Length + 1);\n\n                commandHandler.Invoke(sender, parameters);\n                //commandHandler(sender, message.Substring(CommandName.Length + 1));\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/CoopBriefingBox.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing Rampastring.XNAUI;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    /// <summary>\n    /// A box for drawing scenario briefings.\n    /// </summary>\n    class CoopBriefingBox : XNAPanel\n    {\n        private const int MARGIN = 12;\n        private const float ALPHA_RATE = 0.4f;\n\n        public CoopBriefingBox(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        /// <summary>\n        /// The index of the text font.\n        /// </summary>\n        public int FontIndex { get; set; } = 0;\n\n        string text = string.Empty;\n\n        private bool isVisible = true;\n\n        public override void Initialize()\n        {\n            DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET;\n            ClientRectangle = new Rectangle(0, 0, 400, 300);\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 224), 2, 2);\n\n            InputEnabled = false;\n\n            AlphaRate = ALPHA_RATE;\n\n            base.Initialize();\n\n            CenterOnParent();\n        }\n\n        protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n        {\n            switch (key)\n            {\n                case \"FontIndex\":\n                    FontIndex = Conversions.IntFromString(value, 0);\n                    return;\n            }\n\n            base.ParseControlINIAttribute(iniFile, key, value);\n        }\n\n        public void SetFadeVisibility(bool visible)\n        {\n            isVisible = visible;\n        }\n\n        public void SetAlpha(float alpha)\n        {\n            Alpha = alpha;\n        }\n\n        public void SetText(string text)\n        {\n            this.text = Renderer.FixText(text, FontIndex, Width - (MARGIN * 2)).Text;\n            int textHeight = (int)Renderer.GetTextDimensions(this.text, FontIndex).Y;\n            ClientRectangle = new Rectangle(X, 0,\n                Width, textHeight + MARGIN * 2);\n            CenterOnParent();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (isVisible)\n            {\n                AlphaRate = ALPHA_RATE;\n            }\n            else\n            {\n                AlphaRate = -ALPHA_RATE;\n            }\n\n            base.Update(gameTime);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            //base.Draw(gameTime);\n\n            FillControlArea(new Color(0, 0, 0, 224));\n            DrawRectangle(new Rectangle(0, 0, Width, Height), BorderColor);\n            DrawStringWithShadow(text, FontIndex,\n                new Vector2(MARGIN, MARGIN),\n                UISettings.ActiveSettings.AltColor);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs",
    "content": "﻿using System;\nusing System.Timers;\nusing ClientCore;\nusing ClientCore.Extensions;\nusing ClientGUI;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class GameHostInactiveChecker\n    {\n        private readonly WindowManager windowManager;\n        private readonly Timer timer;\n        private bool isWarningShown;\n        private DateTime startTime;\n        private static int WarningSeconds => ClientConfiguration.Instance.InactiveHostWarningMessageSeconds;\n        private static int CloseSeconds => ClientConfiguration.Instance.InactiveHostKickSeconds;\n\n        public event EventHandler CloseEvent;\n\n        public GameHostInactiveChecker(WindowManager windowManager)\n        {\n            this.windowManager = windowManager;\n            timer = new Timer();\n            timer.AutoReset = true;\n            timer.Interval = 1000;\n            timer.Elapsed += TimerOnElapsed;\n        }\n\n        private void TimerOnElapsed(object sender, ElapsedEventArgs e)\n        {\n            double secondsElapsed = (DateTime.UtcNow - startTime).TotalSeconds;\n\n            if (secondsElapsed > WarningSeconds && !isWarningShown)\n                ShowWarning();\n\n            if (secondsElapsed > CloseSeconds)\n                SendCloseEvent();\n        }\n\n        public void Start()\n        {\n            Reset();\n            timer.Start();\n        }\n\n        public void Reset()\n        {\n            startTime = DateTime.UtcNow;\n            isWarningShown = false;\n        }\n\n        public void Stop() => timer.Stop();\n\n        private void SendCloseEvent()\n        {\n            Stop();\n            CloseEvent?.Invoke(this, null);\n        }\n\n        private void ShowWarning()\n        {\n            isWarningShown = true;\n            XNAMessageBox hostInactiveWarningMessageBox = new XNAMessageBox(\n            windowManager,\n                \"Are you still here?\".L10N(\"Client:Main:InactiveHostWarningTitle\"),\n                \"Your game may be closed due to inactivity.\".L10N(\"Client:Main:InactiveHostWarningText\"),\n                XNAMessageBoxButtons.OK\n            );\n            hostInactiveWarningMessageBox.OKClickedAction = box => Reset();\n            hostInactiveWarningMessageBox.Show();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLaunchButton.cs",
    "content": "﻿using ClientGUI;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class GameLaunchButton : XNAClientButton\n    {\n        public GameLaunchButton(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        private StarDisplay starDisplay;\n\n        public void InitStarDisplay(Texture2D[] rankTextures)\n        {\n            if (starDisplay != null)\n                throw new InvalidOperationException(\"The star display is already initialized!\");\n\n            starDisplay = new StarDisplay(WindowManager, rankTextures);\n            starDisplay.InputEnabled = false;\n            AddChild(starDisplay);\n            ClientRectangleUpdated += (e, sender) => UpdateStarPosition();\n            UpdateStarPosition();\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n        }\n\n        public override string Text\n        {\n            get => base.Text;\n            set { base.Text = value; UpdateStarPosition(); }\n        }\n\n        private void UpdateStarPosition()\n        {\n            if (starDisplay == null)\n                return;\n\n            starDisplay.Y = (Height - starDisplay.Height) / 2;\n            starDisplay.X = (Width / 2) + (int)(Renderer.GetTextDimensions(Text, FontIndex).X / 2) + 3;\n        }\n\n        public void SetRank(int rank)\n        {\n            starDisplay.Rank = rank;\n            UpdateStarPosition();\n        }\n    }\n\n    class StarDisplay : XNAControl\n    {\n        public StarDisplay(WindowManager windowManager, Texture2D[] rankTextures) : base(windowManager)\n        {\n            Name = \"StarDisplay\";\n            this.rankTextures = rankTextures;\n            Width = rankTextures[1].Width;\n            Height = rankTextures[1].Height;\n        }\n\n        private readonly Texture2D[] rankTextures;\n\n        public int Rank { get; set; }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            DrawTexture(rankTextures[Rank], Point.Zero, Color.White);\n            base.Draw(gameTime);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLeftEventArgs.cs",
    "content": "﻿#nullable enable\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby;\n\npublic class GameLeftEventArgs\n{\n    public string? Message { get; init; }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs",
    "content": "using ClientCore;\nusing ClientCore.Statistics;\nusing ClientGUI;\nusing DTAClient.Domain;\nusing DTAClient.Domain.Multiplayer;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing ClientCore.Enums;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing DTAClient.Online.EventArguments;\nusing ClientCore.Extensions;\n\nusing DTAClient.DXGUI.Generic;\n\nusing TextCopy;\nusing System.Diagnostics;\n\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    /// <summary>\n    /// A generic base for all game lobbies (Skirmish, LAN and CnCNet).\n    /// Contains the common logic for parsing game options and handling player info.\n    /// </summary>\n    public abstract class GameLobbyBase : INItializableWindow\n    {\n        protected record Rank\n        {\n            private readonly int rank;\n\n            public static readonly Rank None = 0;\n            public static readonly Rank Easy = 1;\n            public static readonly Rank Medium = 2;\n            public static readonly Rank Hard = 3;\n\n            private Rank(int rank) => this.rank = rank;\n\n            public static implicit operator int(Rank value) => value.rank;\n\n            public static implicit operator Rank(int value) => new Rank(value);\n        }\n\n        protected const int MAX_PLAYER_COUNT = 8;\n        protected const int PLAYER_OPTION_VERTICAL_MARGIN = 12;\n        protected const int PLAYER_OPTION_HORIZONTAL_MARGIN = 3;\n        protected const int PLAYER_OPTION_CAPTION_Y = 6;\n        private const int DROP_DOWN_HEIGHT = 21;\n        protected readonly string BTN_LAUNCH_GAME = \"Launch Game\".L10N(\"Client:Main:ButtonLaunchGame\");\n        protected readonly string BTN_LAUNCH_READY = \"I'm Ready\".L10N(\"Client:Main:ButtonIAmReady\");\n        protected readonly string BTN_LAUNCH_NOT_READY = \"Not Ready\".L10N(\"Client:Main:ButtonNotReady\");\n\n        private readonly string FavoriteMapsLabel = \"Favorites\".L10N(\"Client:Main:Favorites\");\n\n        /// <summary>\n        /// Creates a new instance of the game lobby base.\n        /// </summary>\n        /// <param name=\"windowManager\"></param>\n        /// <param name=\"iniName\">The name of the lobby in GameOptions.ini.</param>\n        /// <param name=\"mapLoader\"></param>\n        /// <param name=\"isMultiplayer\"></param>\n        /// <param name=\"discordHandler\"></param>\n        public GameLobbyBase(\n            WindowManager windowManager,\n            string iniName,\n            MapLoader mapLoader,\n            bool isMultiplayer,\n            DiscordHandler discordHandler,\n            Random random\n        ) : base(windowManager)\n        {\n            _iniSectionName = iniName;\n            MapLoader = mapLoader;\n            this.isMultiplayer = isMultiplayer;\n            this.discordHandler = discordHandler;\n            this.random = random;\n        }\n\n        private string _iniSectionName;\n\n        private Random random;\n\n        protected XNAPanel PlayerOptionsPanel;\n\n        protected List<MultiplayerColor> MPColors;\n\n        public List<GameLobbyCheckBox> CheckBoxes { get; } = new();\n        public List<GameLobbyDropDown> DropDowns { get; } = new();\n\n        public List<IGameSessionSetting> GetBroadcastableSettings()\n        {\n            var result = new List<IGameSessionSetting>();\n\n            result.AddRange(CheckBoxes.Where(cb => cb.BroadcastToLobby));\n            result.AddRange(DropDowns.Where(dd => dd.BroadcastToLobby));\n\n            return result;\n        }\n\n        protected DiscordHandler discordHandler;\n\n        protected MapLoader MapLoader;\n        /// <summary>\n        /// The list of multiplayer game mode maps.\n        /// Each is an instance of a map for a specific game mode.\n        /// </summary>\n        protected IReadOnlyGameModeMapCollection GameModeMaps => MapLoader.GameModeMaps;\n\n        protected GameModeMapFilter gameModeMapFilter;\n\n        private GameModeMap _gameModeMap;\n\n        /// <summary>\n        /// The currently selected game mode.\n        /// </summary>\n        protected GameModeMap GameModeMap\n        {\n            get => _gameModeMap;\n            set\n            {\n                var oldGameModeMap = _gameModeMap;\n                _gameModeMap = value;\n                if (value != null && oldGameModeMap != value)\n                    UpdateDiscordPresence();\n            }\n        }\n\n        protected Map Map => GameModeMap?.Map;\n        protected GameMode GameMode => GameModeMap?.GameMode;\n\n        protected XNAClientDropDown[] ddPlayerNames;\n        protected XNAClientDropDown[] ddPlayerSides;\n        protected XNAClientColorDropDown[] ddPlayerColors;\n        protected XNAClientDropDown[] ddPlayerStarts;\n        protected XNAClientDropDown[] ddPlayerTeams;\n\n        protected XNAClientButton btnPlayerExtraOptionsOpen;\n        protected PlayerExtraOptionsPanel PlayerExtraOptionsPanel;\n\n        protected XNAClientButton btnLeaveGame;\n        protected GameLaunchButton btnLaunchGame;\n        protected XNAClientButton btnPickRandomMap;\n        protected XNALabel lblMapName;\n        protected XNALabel lblMapAuthor;\n        protected XNALabel lblGameMode;\n        protected XNALabel lblMapSize;\n\n        protected MapPreviewBox MapPreviewBox;\n\n        protected XNAMultiColumnListBox lbGameModeMapList;\n        protected ToolTip mapListTooltip;\n        protected XNAClientDropDown ddGameModeMapFilter;\n        protected XNALabel lblGameModeSelect;\n        protected XNAContextMenu mapContextMenu;\n        private XNAContextMenuItem toggleFavoriteItem;\n\n        protected XNAClientStateButton<SortDirection> btnMapSortAlphabetically;\n\n        protected XNASuggestionTextBox tbMapSearch;\n        protected XNAContextMenu searchContextMenu;\n        private XNAContextMenuItem searchCurrentModeItem;\n        private XNAContextMenuItem searchAllModesItem;\n        private bool searchAllGameModes = false;\n\n        protected List<PlayerInfo> Players = new List<PlayerInfo>();\n        protected List<PlayerInfo> AIPlayers = new List<PlayerInfo>();\n\n        protected virtual PlayerInfo FindLocalPlayer() => Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n\n        protected bool PlayerUpdatingInProgress { get; set; }\n\n        protected Texture2D[] RankTextures;\n\n        /// <summary>\n        /// The seed used for randomizing player options.\n        /// </summary>\n        protected int RandomSeed { get; set; }\n\n        /// <summary>\n        /// An unique identifier for this game.\n        /// </summary>\n        protected int UniqueGameID { get; set; }\n        protected int SideCount { get; private set; }\n        protected int RandomSelectorCount { get; private set; } = 1;\n\n        /// <summary>\n        /// The maximum number of players allowed in this lobby.\n        /// </summary>\n        protected virtual int MaxPlayerCount => MAX_PLAYER_COUNT;\n\n        protected List<int[]> RandomSelectors = new List<int[]>();\n\n        private readonly bool isMultiplayer = false;\n\n        private MatchStatistics matchStatistics;\n\n        private bool disableGameOptionUpdateBroadcast = false;\n\n        protected EventHandler<MultiplayerNameRightClickedEventArgs> MultiplayerNameRightClicked;\n\n        /// <summary>\n        /// If set, the client will remove all starting waypoints from the map\n        /// before launching it.\n        /// </summary>\n        protected bool RemoveStartingLocations { get; set; } = false;\n        protected IniFile GameOptionsIni { get; private set; }\n\n        protected XNAClientButton btnSaveLoadGameOptions { get; set; }\n\n        private XNAContextMenu loadSaveGameOptionsMenu { get; set; }\n\n        private LoadOrSaveGameOptionPresetWindow loadOrSaveGameOptionPresetWindow;\n\n        public override void Initialize()\n        {\n            Name = _iniSectionName;\n            //if (WindowManager.RenderResolutionY < 800)\n            //    ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY);\n            //else\n            ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 60, WindowManager.RenderResolutionY - 32);\n            WindowManager.CenterControlOnScreen(this);\n            BackgroundTexture = AssetLoader.LoadTexture(\"gamelobbybg.png\");\n\n            RankTextures = new Texture2D[4]\n            {\n                AssetLoader.LoadTexture(\"rankNone.png\"),\n                AssetLoader.LoadTexture(\"rankEasy.png\"),\n                AssetLoader.LoadTexture(\"rankNormal.png\"),\n                AssetLoader.LoadTexture(\"rankHard.png\")\n            };\n\n            MPColors = MultiplayerColor.LoadColors();\n\n            GameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ClientConfiguration.GAME_OPTIONS));\n\n            base.Initialize();\n\n            try\n            {\n                PlayerOptionsPanel = FindChild<XNAPanel>(nameof(PlayerOptionsPanel));\n            }\n            catch (Exception ex)\n            {\n                throw new Exception(string.Format((\"It seems the client configuration was not migrated to accommodate \" +\n                                                   \"for the 'Tiberian Sun Client v6 Changes'.\\n\\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\" +\n                                                   \"Error message: {1}\").L10N(\"Client:Main:NotMigratedClientException\"),\n                                                   \"https://github.com/CnCNet/xna-cncnet-client/\",\n                                                   ex.Message));\n            }\n\n            btnLeaveGame = FindChild<XNAClientButton>(nameof(btnLeaveGame));\n            btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick;\n\n            btnLaunchGame = FindChild<GameLaunchButton>(nameof(btnLaunchGame));\n            btnLaunchGame.LeftClick += BtnLaunchGame_LeftClick;\n            btnLaunchGame.InitStarDisplay(RankTextures);\n\n            MapPreviewBox = FindChild<MapPreviewBox>(\"MapPreviewBox\");\n            MapPreviewBox.SetFields(Players, AIPlayers, MPColors, GameOptionsIni.GetStringValue(\"General\", \"Sides\", String.Empty).Split(','), GameOptionsIni);\n            MapPreviewBox.ToggleFavorite += MapPreviewBox_ToggleFavorite;\n\n            lblMapName = FindChild<XNALabel>(nameof(lblMapName));\n            lblMapAuthor = FindChild<XNALabel>(nameof(lblMapAuthor));\n            lblGameMode = FindChild<XNALabel>(nameof(lblGameMode));\n            lblMapSize = FindChild<XNALabel>(nameof(lblMapSize));\n\n            lbGameModeMapList = FindChild<XNAMultiColumnListBox>(\"lbMapList\"); // lbMapList for backwards compatibility\n            lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged;\n            lbGameModeMapList.RightClick += LbGameModeMapList_RightClick;\n            lbGameModeMapList.AllowKeyboardInput = true; //!isMultiplayer\n\n            mapListTooltip = new(WindowManager, masterControl: lbGameModeMapList);\n            mapListTooltip.FollowCursor = true;\n            lbGameModeMapList.HoveredIndexChanged += LbGameModeMapList_HoveredIndexChanged;\n\n            mapContextMenu = new XNAContextMenu(WindowManager);\n            mapContextMenu.Name = nameof(mapContextMenu);\n            mapContextMenu.Width = 192; // TODO autosizing\n\n            mapContextMenu.AddItem(\"Favorite\".L10N(\"Client:Main:Favorite\"),\n                selectAction: ToggleFavoriteMap);\n            toggleFavoriteItem = mapContextMenu.Items.First();\n\n            mapContextMenu.AddItem(\"Copy Map Name\".L10N(\"Client:Main:CopyMapName\"),\n                selectAction: () => ClipboardService.SetText(Map?.Name));\n            mapContextMenu.AddItem(\"Copy Original Name\".L10N(\"Client:Main:CopyOriginalMapName\"),\n                selectAction: () => ClipboardService.SetText(Map?.UntranslatedName),\n                visibilityChecker: () => Map?.UntranslatedName != Map?.Name);\n            mapContextMenu.AddItem(\"Delete Map\".L10N(\"Client:Main:DeleteMap\"),\n                selectAction: DeleteMapConfirmation,\n                visibilityChecker: CanDeleteMap);\n            mapContextMenu.AddItem(\"Show in folder\".L10N(\"Client:Main:ShowInFolder\"),\n                selectAction: ShowInFolder);\n\n            AddChild(mapContextMenu);\n\n            XNAPanel rankHeader = new XNAPanel(WindowManager);\n            rankHeader.BackgroundTexture = AssetLoader.LoadTexture(\"rank.png\");\n            rankHeader.ClientRectangle = new Rectangle(0, 0, rankHeader.BackgroundTexture.Width,\n                19);\n\n            XNAListBox rankListBox = new XNAListBox(WindowManager);\n            rankListBox.TextBorderDistance = 2;\n\n            lbGameModeMapList.AddColumn(rankHeader, rankListBox);\n            lbGameModeMapList.AddColumn(\"MAP NAME\".L10N(\"Client:Main:MapNameHeader\"), lbGameModeMapList.Width - RankTextures[1].Width - 3);\n\n            ddGameModeMapFilter = FindChild<XNAClientDropDown>(\"ddGameMode\"); // ddGameMode for backwards compatibility\n            ddGameModeMapFilter.SelectedIndexChanged += DdGameModeMapFilter_SelectedIndexChanged;\n\n            ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps)));\n            foreach (GameMode gm in GameModeMaps.GameModes)\n                ddGameModeMapFilter.AddItem(CreateGameFilterItem(gm.UIName, new GameModeMapFilter(GetGameModeMaps(gm))));\n\n            lblGameModeSelect = FindChild<XNALabel>(nameof(lblGameModeSelect));\n\n            InitBtnMapSort();\n\n            tbMapSearch = FindChild<XNASuggestionTextBox>(nameof(tbMapSearch));\n            tbMapSearch.InputReceived += TbMapSearch_InputReceived;\n            tbMapSearch.RightClick += TbMapSearch_RightClick;\n\n            searchContextMenu = new XNAContextMenu(WindowManager);\n            searchContextMenu.Name = nameof(searchContextMenu);\n            searchContextMenu.Width = 150;\n\n            searchCurrentModeItem = new XNAContextMenuItem()\n            {\n                Text = \"Search current mode\".L10N(\"Client:Main:SearchCurrentMode\"),\n                SelectAction = () => SetSearchAllGameModes(false),\n                HintTextGenerator = () => !searchAllGameModes ? \"< \" : null\n            };\n            searchContextMenu.AddItem(searchCurrentModeItem);\n\n            searchAllModesItem = new XNAContextMenuItem()\n            {\n                Text = \"Search all modes\".L10N(\"Client:Main:SearchAllModes\"),\n                SelectAction = () => SetSearchAllGameModes(true),\n                HintTextGenerator = () => searchAllGameModes ? \"< \" : null\n            };\n            searchContextMenu.AddItem(searchAllModesItem);\n            searchAllGameModes = UserINISettings.Instance.SearchAllGameModes.Value;\n\n            AddChild(searchContextMenu);\n\n\n            btnPickRandomMap = FindChild<XNAClientButton>(nameof(btnPickRandomMap));\n            btnPickRandomMap.LeftClick += BtnPickRandomMap_LeftClick;\n\n            CheckBoxes.ForEach(chk => chk.CheckedChanged += ChkBox_CheckedChanged);\n            DropDowns.ForEach(dd => dd.SelectedIndexChanged += Dropdown_SelectedIndexChanged);\n\n            InitializeGameOptionPresetUI();\n        }\n\n        /// <summary>\n        /// Until the GUICreator can handle typed classes, this must remain manually done.\n        /// </summary>\n        private void InitBtnMapSort()\n        {\n            btnMapSortAlphabetically = new XNAClientStateButton<SortDirection>(WindowManager, new Dictionary<SortDirection, Texture2D>()\n            {\n                { SortDirection.None, AssetLoader.LoadTexture(\"sortAlphaNone.png\") },\n                { SortDirection.Asc, AssetLoader.LoadTexture(\"sortAlphaAsc.png\") },\n                { SortDirection.Desc, AssetLoader.LoadTexture(\"sortAlphaDesc.png\") },\n            });\n            btnMapSortAlphabetically.Name = nameof(btnMapSortAlphabetically);\n            btnMapSortAlphabetically.ClientRectangle = new Rectangle(\n                ddGameModeMapFilter.X + -ddGameModeMapFilter.Height - 4, ddGameModeMapFilter.Y,\n                ddGameModeMapFilter.Height, ddGameModeMapFilter.Height\n            );\n            btnMapSortAlphabetically.LeftClick += BtnMapSortAlphabetically_LeftClick;\n            btnMapSortAlphabetically.SetToolTipText(\"Sort Maps Alphabetically\".L10N(\"Client:Main:MapSortAlphabeticallyToolTip\"));\n            RefreshMapSortAlphabeticallyBtn();\n            AddChild(btnMapSortAlphabetically);\n\n            // Allow repositioning / disabling in INI.\n            ReadINIForControl(btnMapSortAlphabetically);\n\n            MapLoader.MapChanged += MapLoader_MapChanged;\n        }\n\n        private void InitializeGameOptionPresetUI()\n        {\n            btnSaveLoadGameOptions = FindChild<XNAClientButton>(nameof(btnSaveLoadGameOptions), true);\n\n            if (btnSaveLoadGameOptions != null)\n            {\n                loadOrSaveGameOptionPresetWindow = new LoadOrSaveGameOptionPresetWindow(WindowManager);\n                loadOrSaveGameOptionPresetWindow.Name = nameof(loadOrSaveGameOptionPresetWindow);\n                loadOrSaveGameOptionPresetWindow.PresetLoaded += (sender, s) => HandleGameOptionPresetLoadCommand(s);\n                loadOrSaveGameOptionPresetWindow.PresetSaved += (sender, s) => HandleGameOptionPresetSaveCommand(s);\n                loadOrSaveGameOptionPresetWindow.Disable();\n                var loadConfigMenuItem = new XNAContextMenuItem()\n                {\n                    Text = \"Load\".L10N(\"Client:Main:ButtonLoad\"),\n                    SelectAction = () => loadOrSaveGameOptionPresetWindow.Show(true)\n                };\n                var saveConfigMenuItem = new XNAContextMenuItem()\n                {\n                    Text = \"Save\".L10N(\"Client:Main:ButtonSave\"),\n                    SelectAction = () => loadOrSaveGameOptionPresetWindow.Show(false)\n                };\n\n                loadSaveGameOptionsMenu = new XNAContextMenu(WindowManager);\n                loadSaveGameOptionsMenu.Name = nameof(loadSaveGameOptionsMenu);\n                loadSaveGameOptionsMenu.ClientRectangle = new Rectangle(0, 0, 75, 0);\n                loadSaveGameOptionsMenu.Items.Add(loadConfigMenuItem);\n                loadSaveGameOptionsMenu.Items.Add(saveConfigMenuItem);\n\n                btnSaveLoadGameOptions.LeftClick += (sender, args) =>\n                    loadSaveGameOptionsMenu.Open(GetCursorPoint());\n\n                AddChild(loadSaveGameOptionsMenu);\n                AddChild(loadOrSaveGameOptionPresetWindow);\n            }\n        }\n\n        private void BtnMapSortAlphabetically_LeftClick(object sender, EventArgs e)\n        {\n            UserINISettings.Instance.MapSortState.Value = (int)btnMapSortAlphabetically.GetState();\n\n            RefreshMapSortAlphabeticallyBtn();\n            UserINISettings.Instance.SaveSettings();\n            ListMaps();\n        }\n\n        private void RefreshMapSortAlphabeticallyBtn()\n        {\n            if (Enum.IsDefined(typeof(SortDirection), UserINISettings.Instance.MapSortState.Value))\n                btnMapSortAlphabetically.SetState((SortDirection)UserINISettings.Instance.MapSortState.Value);\n        }\n\n        private void MapLoader_MapChanged(object sender, MapChangedEventArgs e)\n        {\n            WindowManager.AddCallback(() =>\n            {\n                switch (e.ChangeType)\n                {\n                    case MapChangeType.Added:\n                        HandleMapAdded(e.Map);\n                        break;\n                    case MapChangeType.Updated:\n                        HandleMapUpdated(e.Map, e.PreviousMapSHA1);\n                        break;\n                    case MapChangeType.Removed:\n                        HandleMapRemoved(e.Map);\n                        break;\n                }\n            }, null);\n        }\n\n        protected virtual void HandleMapAdded(Map addedMap)\n        {\n            RefreshGameModeFilter();\n\n            if (ShouldShowMapInCurrentFilter(addedMap))\n                ListMaps();\n        }\n\n        protected virtual void HandleMapUpdated(Map updatedMap, string previousSHA1)\n        {\n            // If the currently selected map was updated, refresh the UI\n            if (Map != null && (Map.SHA1 == previousSHA1 || Map.SHA1 == updatedMap.SHA1))\n            {\n                // Find the new GameModeMap for the updated map\n                var updatedGameModeMap = GameModeMaps\n                    .FirstOrDefault(gmm => gmm.Map.SHA1 == updatedMap.SHA1);\n\n                if (updatedGameModeMap != null)\n                    ChangeMap(updatedGameModeMap);\n            }\n\n            ListMaps();\n        }\n\n        private void HandleMapRemoved(Map removedMap)\n        {\n            // If the currently selected map was removed, select a different one\n            if (Map != null && Map.SHA1 == removedMap.SHA1)\n            {\n                var availableMaps = GameModeMaps.Where(gmm => gmm.GameMode == GameMode).ToList();\n                if (availableMaps.Any())\n                {\n                    ChangeMap(availableMaps.First());\n                }\n                else\n                {\n                    // No maps available for current game mode, change to a different one\n                    var firstAvailableGameModeMap = GameModeMaps.FirstOrDefault();\n                    if (firstAvailableGameModeMap != null)\n                    {\n                        ChangeMap(firstAvailableGameModeMap);\n                        RefreshMapSelectionUI();\n                    }\n                }\n            }\n\n            RefreshGameModeFilter();\n            ListMaps();\n        }\n\n        private bool ShouldShowMapInCurrentFilter(Map map)\n        {\n            if (map?.GameModes == null || gameModeMapFilter == null)\n                return false;\n\n            return map.GameModes.Any(gameModeName =>\n            {\n                var gameMode = MapLoader.GameModes.FirstOrDefault(gm => gm.Name == gameModeName);\n                if (gameMode == null) return false;\n\n                return gameModeMapFilter.GetGameModeMaps().Any(gmm =>\n                    gmm.GameMode.Name == gameMode.Name && gmm.Map.SHA1 == map.SHA1);\n            });\n        }\n\n        private static XNADropDownItem CreateGameFilterItem(string text, GameModeMapFilter filter)\n        {\n            return new XNADropDownItem\n            {\n                Text = text,\n                Tag = filter\n            };\n        }\n\n        protected bool IsFavoriteMapsSelected() => ddGameModeMapFilter.SelectedItem?.Text == FavoriteMapsLabel;\n\n        private List<GameModeMap> GetFavoriteGameModeMaps() =>\n            GameModeMaps.Where(gmm => gmm.IsFavorite).ToList();\n\n        private Func<List<GameModeMap>> GetGameModeMaps(GameMode gm) => () =>\n            GameModeMaps.Where(gmm => gmm.GameMode == gm).ToList();\n\n        private void RefreshBtnPlayerExtraOptionsOpenTexture()\n        {\n            if (btnPlayerExtraOptionsOpen != null)\n            {\n                var textureName = GetPlayerExtraOptions().IsDefault() ? \"optionsButton.png\" : \"optionsButtonActive.png\";\n                var hoverTextureName = GetPlayerExtraOptions().IsDefault() ? \"optionsButton_c.png\" : \"optionsButtonActive_c.png\";\n                var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null;\n                btnPlayerExtraOptionsOpen.IdleTexture = AssetLoader.LoadTexture(textureName);\n                btnPlayerExtraOptionsOpen.HoverTexture = hoverTexture;\n            }\n        }\n\n        protected void HandleGameOptionPresetSaveCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetSaveCommand(e.PresetName);\n\n        protected void HandleGameOptionPresetSaveCommand(string presetName)\n        {\n            string error = AddGameOptionPreset(presetName);\n            if (!string.IsNullOrEmpty(error))\n                AddNotice(error);\n        }\n\n        protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommand(e.PresetName);\n\n        protected void HandleGameOptionPresetLoadCommand(string presetName)\n        {\n            if (LoadGameOptionPreset(presetName))\n                AddNotice(\"Game option preset loaded succesfully.\".L10N(\"Client:Main:PresetLoaded\"));\n            else\n                AddNotice(string.Format(\"Preset {0} not found!\".L10N(\"Client:Main:PresetNotFound\"), presetName));\n        }\n\n        protected void AddNotice(string message) => AddNotice(message, Color.White);\n\n        protected abstract void AddNotice(string message, Color color);\n\n        private void BtnPickRandomMap_LeftClick(object sender, EventArgs e) => PickRandomMap();\n\n        private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps();\n\n        private void TbMapSearch_RightClick(object sender, EventArgs e) => searchContextMenu.Open(GetCursorPoint());\n\n        private void SetSearchAllGameModes(bool value)\n        {\n            searchAllGameModes = value;\n            UserINISettings.Instance.SearchAllGameModes.Value = value;\n            UserINISettings.Instance.SaveSettings();\n            ListMaps();\n        }\n\n        private void Dropdown_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            if (disableGameOptionUpdateBroadcast)\n                return;\n\n            var dd = (GameLobbyDropDown)sender;\n            dd.HostSelectedIndex = dd.SelectedIndex;\n            OnGameOptionChanged();\n        }\n\n        private void ChkBox_CheckedChanged(object sender, EventArgs e)\n        {\n            if (disableGameOptionUpdateBroadcast)\n                return;\n\n            var checkBox = (GameLobbyCheckBox)sender;\n            checkBox.HostChecked = checkBox.Checked;\n            OnGameOptionChanged();\n        }\n\n        protected virtual void OnGameOptionChanged()\n        {\n            CheckDisallowedSides();\n\n            btnLaunchGame.SetRank(GetRank());\n        }\n\n        protected void DdGameModeMapFilter_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter;\n\n            tbMapSearch.Text = string.Empty;\n            tbMapSearch.OnSelectedChanged();\n\n            ListMaps();\n\n            if (lbGameModeMapList.SelectedIndex == -1)\n                lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap\n            else\n                ChangeMap(GameModeMap);\n        }\n\n        protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e)\n        {\n            if (PlayerExtraOptionsPanel.Enabled)\n                PlayerExtraOptionsPanel.Disable();\n            else\n                PlayerExtraOptionsPanel.Enable();\n        }\n\n        protected void ApplyPlayerExtraOptions(string sender, string message)\n        {\n            var playerExtraOptions = PlayerExtraOptions.FromMessage(message);\n\n            if (PlayerExtraOptionsPanel != null)\n            {\n                if (playerExtraOptions.IsForceRandomSides != PlayerExtraOptionsPanel.ForcedRandomSides)\n                    AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomSides, \"side selection\".L10N(\"Client:Main:SideAsANoun\"));\n\n                if (playerExtraOptions.IsForceRandomColors != PlayerExtraOptionsPanel.ForcedRandomColors)\n                    AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomColors, \"color selection\".L10N(\"Client:Main:ColorAsANoun\"));\n\n                if (playerExtraOptions.IsForceRandomStarts != PlayerExtraOptionsPanel.ForcedRandomStarts)\n                    AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomStarts, \"start selection\".L10N(\"Client:Main:StartPositionAsANoun\"));\n\n                if (playerExtraOptions.IsForceNoTeams != PlayerExtraOptionsPanel.ForcedNoTeams)\n                    AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceNoTeams, \"team selection\".L10N(\"Client:Main:TeamAsANoun\"));\n\n                if (playerExtraOptions.IsUseTeamStartMappings != PlayerExtraOptionsPanel.UseTeamStartMappings)\n                    AddPlayerExtraOptionForcedNotice(!playerExtraOptions.IsUseTeamStartMappings, \"auto ally\".L10N(\"Client:Main:AutoAllyAsANoun\"));\n            }\n\n            SetPlayerExtraOptions(playerExtraOptions);\n            UpdateMapPreviewBoxEnabledStatus();\n        }\n\n        private void AddPlayerExtraOptionForcedNotice(bool disabled, string type)\n            => AddNotice(disabled ?\n                string.Format(\"The game host has disabled {0}\".L10N(\"Client:Main:HostDisableSection\"), type) :\n                string.Format(\"The game host has enabled {0}\".L10N(\"Client:Main:HostEnableSection\"), type));\n\n        protected List<GameModeMap> GetSortedGameModeMaps()\n        {\n            var gameModeMaps = searchAllGameModes ? GameModeMaps.ToList() : gameModeMapFilter.GetGameModeMaps();\n\n            // Only apply sort if the map list sort button is available.\n            if (btnMapSortAlphabetically.Enabled && btnMapSortAlphabetically.Visible)\n            {\n                switch ((SortDirection)UserINISettings.Instance.MapSortState.Value)\n                {\n                    case SortDirection.Asc:\n                        gameModeMaps = gameModeMaps.OrderBy(gmm => gmm.Map.Name).ToList();\n                        break;\n                    case SortDirection.Desc:\n                        gameModeMaps = gameModeMaps.OrderByDescending(gmm => gmm.Map.Name).ToList();\n                        break;\n                }\n            }\n\n            return gameModeMaps;\n        }\n\n        protected void ListMaps()\n        {\n            lbGameModeMapList.SelectedIndexChanged -= LbGameModeMapList_SelectedIndexChanged;\n\n            lbGameModeMapList.ClearItems();\n            lbGameModeMapList.SetTopIndex(0);\n\n            lbGameModeMapList.SelectedIndex = -1;\n\n            int mapIndex = -1;\n\n            var isFavoriteMapsSelected = IsFavoriteMapsSelected();\n            var maps = GetSortedGameModeMaps();\n\n            bool gameModeMapChanged = false;\n\n            List<GameModeMap> filteredMaps;\n\n            if (tbMapSearch.Text != tbMapSearch.Suggestion)\n            {\n                string search = tbMapSearch.Text.Trim();\n                string[] searchWords = search.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);\n\n                // Equals entire search string\n                var exactMatches = maps.Where(gmm =>\n                    gmm.Map.Name.Equals(search, StringComparison.CurrentCultureIgnoreCase) ||\n                    gmm.Map.UntranslatedName.Equals(search, StringComparison.InvariantCultureIgnoreCase)).ToList();\n\n                // Contains entire search string\n                var substringMatches = maps.Except(exactMatches).Where(gmm =>\n                    gmm.Map.Name.Contains(search, StringComparison.CurrentCultureIgnoreCase) ||\n                    gmm.Map.UntranslatedName.Contains(search, StringComparison.InvariantCultureIgnoreCase)).ToList();\n\n                // Contains all search words. It matches with \"AND\" logic: Word1 AND Word2 AND Word3\n                var multiWordMatches = maps.Except(exactMatches).Except(substringMatches).Where(gmm =>\n                {\n                    bool allInTranslated = searchWords.All(word =>\n                        gmm.Map.Name.Contains(word, StringComparison.CurrentCultureIgnoreCase));\n\n                    bool allInUntranslated = searchWords.All(word =>\n                        gmm.Map.UntranslatedName.Contains(word, StringComparison.InvariantCultureIgnoreCase));\n\n                    return allInTranslated || allInUntranslated;\n                }).ToList();\n\n                filteredMaps = [.. exactMatches, .. substringMatches, .. multiWordMatches];\n            }\n            else\n            {\n                filteredMaps = maps;\n            }\n\n            for (int i = 0; i < filteredMaps.Count; i++)\n            {\n                var gameModeMap = filteredMaps[i];\n\n                XNAListBoxItem rankItem = new XNAListBoxItem();\n                if (gameModeMap.IsCoop)\n                {\n                    // Note: StatisticsManager.Statistics must be initialized to call `HasBeatCoOpMap()`. This means StatisticsWindow must be initialized before any lobbies extending GameLobbyBase.\n                    if (StatisticsManager.Instance.HasBeatCoOpMap(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName))\n                        rankItem.Texture = RankTextures[Math.Abs(2 - gameModeMap.CoopDifficultyLevel) + 1];\n                    else\n                        rankItem.Texture = RankTextures[0];\n                }\n                else\n                    rankItem.Texture = RankTextures[GetDefaultMapRankIndex(gameModeMap) + 1];\n\n                XNAListBoxItem mapNameItem = new XNAListBoxItem();\n                var mapNameText = gameModeMap.Map.Name;\n                if (isFavoriteMapsSelected || searchAllGameModes)\n                    mapNameText += $\" - {gameModeMap.GameMode.UIName}\";\n\n                mapNameItem.Text = Renderer.GetSafeString(mapNameText, lbGameModeMapList.FontIndex);\n\n                if (gameModeMap.MultiplayerOnly && !isMultiplayer)\n                    mapNameItem.TextColor = UISettings.ActiveSettings.DisabledItemColor;\n                mapNameItem.Tag = gameModeMap;\n\n                XNAListBoxItem[] mapInfoArray = {\n                    rankItem,\n                    mapNameItem,\n                };\n\n                lbGameModeMapList.AddItem(mapInfoArray);\n\n                // Preserve the selected map\n                if (gameModeMap == GameModeMap)\n                {\n                    mapIndex = i;\n                    gameModeMapChanged = false;\n                }\n\n                if (mapIndex == -1 && (gameModeMap?.Map?.Equals(GameModeMap?.Map) ?? false))\n                {\n                    mapIndex = i;\n                    gameModeMapChanged = true;\n                }\n            }\n\n            if (mapIndex > -1)\n            {\n                lbGameModeMapList.SelectedIndex = mapIndex;\n                while (mapIndex > lbGameModeMapList.LastIndex)\n                    lbGameModeMapList.TopIndex++;\n            }\n\n            lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged;\n\n            // Trigger the event manually to update GameModeMap\n            if (gameModeMapChanged)\n                LbGameModeMapList_SelectedIndexChanged();\n        }\n\n        protected abstract int GetDefaultMapRankIndex(GameModeMap gameModeMap);\n\n        private void LbGameModeMapList_RightClick(object sender, EventArgs e)\n        {\n            if (lbGameModeMapList.HoveredIndex < 0 || lbGameModeMapList.HoveredIndex >= lbGameModeMapList.ItemCount)\n                return;\n\n            lbGameModeMapList.SelectedIndex = lbGameModeMapList.HoveredIndex;\n\n            if (!mapContextMenu.Items.Any(i => i.VisibilityChecker == null || i.VisibilityChecker()))\n                return;\n\n            toggleFavoriteItem.Text = GameModeMap.IsFavorite ? \"Remove Favorite\".L10N(\"Client:Main:RemoveFavorite\") : \"Add Favorite\".L10N(\"Client:Main:AddFavorite\");\n\n            mapContextMenu.Open(GetCursorPoint());\n        }\n\n        private bool CanDeleteMap()\n        {\n            return Map != null && !Map.Official && !isMultiplayer;\n        }\n\n        private void DeleteMapConfirmation()\n        {\n            if (Map == null)\n                return;\n\n            var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, \"Delete Confirmation\".L10N(\"Client:Main:DeleteMapConfirmTitle\"),\n                string.Format(\"Are you sure you wish to delete the custom map {0}?\".L10N(\"Client:Main:DeleteMapConfirmText\"), Map.Name));\n            messageBox.YesClickedAction = DeleteSelectedMap;\n        }\n\n        private void ShowInFolder() => Map?.OpenContainingFolder();\n\n        private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) =>\n            ToggleFavoriteMap();\n\n        protected virtual void ToggleFavoriteMap()\n        {\n            if (GameModeMap != null)\n            {\n                GameModeMap.IsFavorite = UserINISettings.Instance.ToggleFavoriteMap(Map.SHA1, GameMode.Name, GameModeMap.IsFavorite);\n                MapPreviewBox.RefreshFavoriteBtn();\n            }\n        }\n\n        protected void RefreshForFavoriteMapRemoved()\n        {\n            if (!gameModeMapFilter.GetGameModeMaps().Any())\n            {\n                LoadDefaultGameModeMap();\n                return;\n            }\n\n            ListMaps();\n            if (IsFavoriteMapsSelected())\n                lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites\n        }\n\n        private void DeleteSelectedMap(XNAMessageBox messageBox)\n        {\n            try\n            {\n                MapLoader.DeleteCustomMap(GameModeMap);\n\n                tbMapSearch.Text = string.Empty;\n                if (GameMode.Maps.Count == 0)\n                {\n                    // this will trigger another GameMode to be selected\n                    GameModeMap = GameModeMaps.FirstOrDefault(gm => gm.GameMode.Maps.Count > 0);\n                }\n                else\n                {\n                    // this will trigger another Map to be selected\n                    lbGameModeMapList.SelectedIndex = lbGameModeMapList.SelectedIndex == 0 ? 1 : lbGameModeMapList.SelectedIndex - 1;\n                }\n\n                ListMaps();\n                ChangeMap(GameModeMap);\n            }\n            catch (IOException ex)\n            {\n                Logger.Log($\"Deleting map {Map.BaseFilePath} failed! Message: {ex.ToString()}\");\n                XNAMessageBox.Show(WindowManager, \"Deleting Map Failed\".L10N(\"Client:Main:DeleteMapFailedTitle\"),\n                    \"Deleting map failed! Reason:\".L10N(\"Client:Main:DeleteMapFailedText\") + \" \" + ex.Message);\n            }\n        }\n\n        private void LbGameModeMapList_SelectedIndexChanged()\n        {\n            if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount)\n            {\n                ChangeMap(null);\n                return;\n            }\n\n            XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex);\n\n            GameModeMap gameModeMap = (GameModeMap)item.Tag;\n\n            ChangeMap(gameModeMap);\n        }\n\n        private void LbGameModeMapList_SelectedIndexChanged(object sender, EventArgs e)\n            => LbGameModeMapList_SelectedIndexChanged();\n\n        private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e)\n        {\n            if (lbGameModeMapList.HoveredIndex < 0 || lbGameModeMapList.HoveredIndex >= lbGameModeMapList.ItemCount)\n            {\n                mapListTooltip.Text = string.Empty;\n                return;\n            }\n\n            var gmm = (GameModeMap)lbGameModeMapList.GetItem(1, lbGameModeMapList.HoveredIndex).Tag;\n\n            if (gmm.Map.UntranslatedName != gmm.Map.Name)\n                mapListTooltip.Text = \"Original name:\".L10N(\"Client:Main:OriginalMapName\") + \" \" + gmm.Map.UntranslatedName;\n            else\n                mapListTooltip.Text = string.Empty;\n        }\n\n        private void PickRandomMap()\n        {\n            int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1)\n                   + AIPlayers.Count;\n            List<Map> maps = GetMapList(totalPlayerCount);\n            if (maps.Count < 1)\n                return;\n\n            int randomValue = random.Next(0, maps.Count);\n            bool isFavoriteMapsSelected = IsFavoriteMapsSelected();\n            GameModeMap = GameModeMaps.FirstOrDefault(gmm => (gmm.GameMode == GameMode || gmm.IsFavorite && isFavoriteMapsSelected) && gmm.Map == maps[randomValue]);\n            Logger.Log(\"PickRandomMap: Rolled \" + randomValue + \" out of \" + maps.Count + \". Picked map: \" + Map.Name);\n\n            ChangeMap(GameModeMap);\n            tbMapSearch.Text = string.Empty;\n            tbMapSearch.OnSelectedChanged();\n            ListMaps();\n        }\n\n        private List<Map> GetMapList(int playerCount)\n        {\n            List<Map> maps = IsFavoriteMapsSelected()\n                ? GetFavoriteGameModeMaps().Select(gameModeMap => gameModeMap.Map).ToList()\n                : GameMode?.Maps.ToList() ?? new List<Map>();\n\n            if (playerCount != 1)\n            {\n\n                if (GameMode?.MaxPlayersOverride != null)\n                {\n                    // MaxPlayers have been overridden in GameMode. This means all maps in the game mode has the same MaxPlayers value\n                    if (playerCount != GameMode.MaxPlayersOverride)\n                        maps = [];\n                }\n                else\n                {\n                    // Maps could have different MaxPlayers values.\n                    maps = maps.Where(x => x.MaxPlayers == playerCount).ToList();\n                }\n\n                if (maps.Count < 1 && playerCount <= MAX_PLAYER_COUNT)\n                    return GetMapList(playerCount + 1);\n            }\n\n            return maps;\n        }\n\n        /// <summary>\n        /// Refreshes the game mode filter dropdown to include all current game modes.\n        /// </summary>\n        protected void RefreshGameModeFilter()\n        {\n            string currentSelection = ddGameModeMapFilter.SelectedItem?.Text;\n\n            ddGameModeMapFilter.SelectedIndexChanged -= DdGameModeMapFilter_SelectedIndexChanged;\n            ddGameModeMapFilter.Items.Clear();\n\n            ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps)));\n            foreach (GameMode gm in GameModeMaps.GameModes)\n                ddGameModeMapFilter.AddItem(CreateGameFilterItem(gm.UIName, new GameModeMapFilter(GetGameModeMaps(gm))));\n\n            int selectedIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Text == currentSelection);\n            ddGameModeMapFilter.SelectedIndex = selectedIndex >= 0 ? selectedIndex : 0;\n\n            ddGameModeMapFilter.SelectedIndexChanged += DdGameModeMapFilter_SelectedIndexChanged;\n            gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter;\n        }\n\n        /// <summary>\n        /// Refreshes the map selection UI to match the currently selected map\n        /// and game mode.\n        /// </summary>\n        protected void RefreshMapSelectionUI()\n        {\n            if (GameMode == null)\n                return;\n\n            int gameModeMapFilterIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Text == GameMode.UIName);\n\n            if (gameModeMapFilterIndex == -1)\n                return;\n\n            if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex)\n                DdGameModeMapFilter_SelectedIndexChanged(this, EventArgs.Empty);\n\n            ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex;\n        }\n\n        protected void AddSideToDropDown(XNADropDown dd, string name, string? uiName = null, Texture2D? texture = null)\n        {\n            XNADropDownItem item = new()\n            {\n                Text = uiName ?? name.L10N($\"INI:Sides:{name}\"),\n                Tag = name,\n                Texture = texture ?? LoadTextureOrNull(name + \"icon.png\"),\n            };\n            dd.AddItem(item);\n        }\n\n        /// <summary>\n        /// Initializes the player option drop-down controls.\n        /// </summary>\n        protected void InitPlayerOptionDropdowns()\n        {\n            ddPlayerNames = new XNAClientDropDown[MAX_PLAYER_COUNT];\n            ddPlayerSides = new XNAClientDropDown[MAX_PLAYER_COUNT];\n            ddPlayerColors = new XNAClientColorDropDown[MAX_PLAYER_COUNT];\n            ddPlayerStarts = new XNAClientDropDown[MAX_PLAYER_COUNT];\n            ddPlayerTeams = new XNAClientDropDown[MAX_PLAYER_COUNT];\n\n            int playerOptionVecticalMargin = ConfigIni.GetIntValue(Name, \"PlayerOptionVerticalMargin\", PLAYER_OPTION_VERTICAL_MARGIN);\n            int playerOptionHorizontalMargin = ConfigIni.GetIntValue(Name, \"PlayerOptionHorizontalMargin\", PLAYER_OPTION_HORIZONTAL_MARGIN);\n            int playerOptionCaptionLocationY = ConfigIni.GetIntValue(Name, \"PlayerOptionCaptionLocationY\", PLAYER_OPTION_CAPTION_Y);\n            int playerNameWidth = ConfigIni.GetIntValue(Name, \"PlayerNameWidth\", 136);\n            int sideWidth = ConfigIni.GetIntValue(Name, \"SideWidth\", 91);\n            int colorWidth = ConfigIni.GetIntValue(Name, \"ColorWidth\", 79);\n            int startWidth = ConfigIni.GetIntValue(Name, \"StartWidth\", 49);\n            int teamWidth = ConfigIni.GetIntValue(Name, \"TeamWidth\", 46);\n            int locationX = ConfigIni.GetIntValue(Name, \"PlayerOptionLocationX\", 25);\n            int locationY = ConfigIni.GetIntValue(Name, \"PlayerOptionLocationY\", 24);\n\n            // InitPlayerOptionDropdowns(136, 91, 79, 49, 46, new Point(25, 24));\n\n            string[] sides = ClientConfiguration.Instance.Sides.Split(',').ToArray();\n            SideCount = sides.Length;\n\n            List<string> selectorNames = new();\n            GetRandomSelectors(selectorNames, RandomSelectors);\n            RandomSelectorCount = RandomSelectors.Count + 1;\n            MapPreviewBox.RandomSelectorCount = RandomSelectorCount;\n\n            string randomColor = GameOptionsIni.GetStringValue(\"General\", \"RandomColor\", \"255,255,255\");\n\n            for (int i = MAX_PLAYER_COUNT - 1; i > -1; i--)\n            {\n                var ddPlayerName = new XNAClientDropDown(WindowManager);\n                ddPlayerName.Name = \"ddPlayerName\" + i;\n                ddPlayerName.ClientRectangle = new Rectangle(locationX,\n                    locationY + (DROP_DOWN_HEIGHT + playerOptionVecticalMargin) * i,\n                    playerNameWidth, DROP_DOWN_HEIGHT);\n                ddPlayerName.AddItem(String.Empty);\n                ProgramConstants.AI_PLAYER_NAMES.ForEach(ddPlayerName.AddItem);\n                ddPlayerName.AllowDropDown = true;\n                ddPlayerName.SelectedIndexChanged += CopyPlayerDataFromUI;\n                ddPlayerName.RightClick += MultiplayerName_RightClick;\n                ddPlayerName.Tag = true;\n\n                var ddPlayerSide = new XNAClientDropDown(WindowManager);\n                ddPlayerSide.Name = \"ddPlayerSide\" + i;\n                ddPlayerSide.ClientRectangle = new Rectangle(\n                    ddPlayerName.Right + playerOptionHorizontalMargin,\n                    ddPlayerName.Y, sideWidth, DROP_DOWN_HEIGHT);\n\n                const string randomName = \"Random\";\n                AddSideToDropDown(ddPlayerSide, randomName, randomName.L10N(\"Client:Sides:RandomSide\"), LoadTextureOrNull(\"randomicon.png\"));\n\n                foreach (string randomSelector in selectorNames)\n                    AddSideToDropDown(ddPlayerSide, randomSelector);\n                foreach (string sideName in sides)\n                    AddSideToDropDown(ddPlayerSide, sideName);\n\n                ddPlayerSide.AllowDropDown = false;\n                ddPlayerSide.SelectedIndexChanged += CopyPlayerDataFromUI;\n                ddPlayerSide.Tag = true;\n\n                var ddPlayerColor = new XNAClientColorDropDown(WindowManager);\n                ddPlayerColor.Name = \"ddPlayerColor\" + i;\n                ddPlayerColor.ClientRectangle = new Rectangle(\n                    ddPlayerSide.Right + playerOptionHorizontalMargin,\n                    ddPlayerName.Y, colorWidth, DROP_DOWN_HEIGHT);\n                ddPlayerColor.AddItem(\"Random\".L10N(\"Client:Main:RandomColor\"), AssetLoader.GetColorFromString(randomColor));\n                foreach (MultiplayerColor mpColor in MPColors)\n                    ddPlayerColor.AddItem(mpColor.Name, mpColor.XnaColor);\n                ddPlayerColor.AllowDropDown = false;\n                ddPlayerColor.SelectedIndexChanged += CopyPlayerDataFromUI;\n                ddPlayerColor.Tag = false;\n\n                var ddPlayerTeam = new XNAClientDropDown(WindowManager);\n                ddPlayerTeam.Name = \"ddPlayerTeam\" + i;\n                ddPlayerTeam.ClientRectangle = new Rectangle(\n                    ddPlayerColor.Right + playerOptionHorizontalMargin,\n                    ddPlayerName.Y, teamWidth, DROP_DOWN_HEIGHT);\n                ddPlayerTeam.AddItem(\"-\");\n                ProgramConstants.TEAMS.ForEach(ddPlayerTeam.AddItem);\n                ddPlayerTeam.AllowDropDown = false;\n                ddPlayerTeam.SelectedIndexChanged += CopyPlayerDataFromUI;\n                ddPlayerTeam.Tag = true;\n\n                var ddPlayerStart = new XNAClientDropDown(WindowManager);\n                ddPlayerStart.Name = \"ddPlayerStart\" + i;\n                ddPlayerStart.ClientRectangle = new Rectangle(\n                    ddPlayerTeam.Right + playerOptionHorizontalMargin,\n                    ddPlayerName.Y, startWidth, DROP_DOWN_HEIGHT);\n                for (int j = 1; j <= MAX_PLAYER_COUNT; j++)\n                    ddPlayerStart.AddItem(j.ToString());\n                ddPlayerStart.AllowDropDown = false;\n                ddPlayerStart.SelectedIndexChanged += CopyPlayerDataFromUI;\n                ddPlayerStart.Visible = false;\n                ddPlayerStart.Enabled = false;\n                ddPlayerStart.Tag = true;\n\n                ddPlayerNames[i] = ddPlayerName;\n                ddPlayerSides[i] = ddPlayerSide;\n                ddPlayerColors[i] = ddPlayerColor;\n                ddPlayerStarts[i] = ddPlayerStart;\n                ddPlayerTeams[i] = ddPlayerTeam;\n\n                PlayerOptionsPanel.AddChild(ddPlayerName);\n                PlayerOptionsPanel.AddChild(ddPlayerSide);\n                PlayerOptionsPanel.AddChild(ddPlayerColor);\n                PlayerOptionsPanel.AddChild(ddPlayerStart);\n                PlayerOptionsPanel.AddChild(ddPlayerTeam);\n\n                ReadINIForControl(ddPlayerName);\n                ReadINIForControl(ddPlayerSide);\n                ReadINIForControl(ddPlayerColor);\n                ReadINIForControl(ddPlayerStart);\n                ReadINIForControl(ddPlayerTeam);\n            }\n\n            var lblName = GeneratePlayerOptionCaption(\"lblName\", \"PLAYER\".L10N(\"Client:Main:PlayerOptionPlayer\"), ddPlayerNames[0].X, playerOptionCaptionLocationY);\n            var lblSide = GeneratePlayerOptionCaption(\"lblSide\", \"SIDE\".L10N(\"Client:Main:PlayerOptionSide\"), ddPlayerSides[0].X, playerOptionCaptionLocationY);\n            var lblColor = GeneratePlayerOptionCaption(\"lblColor\", \"COLOR\".L10N(\"Client:Main:PlayerOptionColor\"), ddPlayerColors[0].X, playerOptionCaptionLocationY);\n\n            var lblStart = GeneratePlayerOptionCaption(\"lblStart\", \"START\".L10N(\"Client:Main:PlayerOptionStart\"), ddPlayerStarts[0].X, playerOptionCaptionLocationY);\n            lblStart.Visible = false;\n\n            var lblTeam = GeneratePlayerOptionCaption(\"lblTeam\", \"TEAM\".L10N(\"Client:Main:PlayerOptionTeam\"), ddPlayerTeams[0].X, playerOptionCaptionLocationY);\n\n            ReadINIForControl(lblName);\n            ReadINIForControl(lblSide);\n            ReadINIForControl(lblColor);\n            ReadINIForControl(lblStart);\n            ReadINIForControl(lblTeam);\n\n            btnPlayerExtraOptionsOpen = FindChild<XNAClientButton>(nameof(btnPlayerExtraOptionsOpen), true);\n\n            if (btnPlayerExtraOptionsOpen != null)\n            {\n                PlayerExtraOptionsPanel = FindChild<PlayerExtraOptionsPanel>(nameof(PlayerExtraOptionsPanel));\n                ReadINIForControl(PlayerExtraOptionsPanel);\n\n                foreach (var child in PlayerExtraOptionsPanel.Children)\n                {\n                    ReadINIForControl(child);\n                }\n\n                PlayerExtraOptionsPanel.Disable();\n                PlayerExtraOptionsPanel.OptionsChanged += PlayerExtraOptions_OptionsChanged;\n                btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick;\n            }\n\n            CheckDisallowedSides();\n        }\n\n        private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, int y)\n        {\n            var label = new XNALabel(WindowManager);\n            label.Name = name;\n            label.Text = text;\n            label.FontIndex = 1;\n            label.ClientRectangle = new Rectangle(x, y, 0, 0);\n            PlayerOptionsPanel.AddChild(label);\n\n            return label;\n        }\n\n        protected virtual void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e)\n        {\n            var playerExtraOptions = GetPlayerExtraOptions();\n\n            for (int i = 0; i < MAX_PLAYER_COUNT; i++)\n            {\n                var pInfo = GetPlayerInfoForIndex(i);\n\n                // IsForceRandomSides\n                if (pInfo != null && playerExtraOptions.IsForceRandomSides)\n                    pInfo.SideId = 0;\n\n                EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides);\n\n                // IsForceNoTeams\n                Debug.Assert(!playerExtraOptions.IsForceNoTeams || !GameModeMap.IsCoop, \"Co-ops should not have force no teams enabled.\");\n                if (pInfo != null && playerExtraOptions.IsForceNoTeams)\n                    pInfo.TeamId = 0;\n\n                EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceNoTeams);\n\n                // IsForceRandomColors\n                if (pInfo != null && playerExtraOptions.IsForceRandomColors)\n                    pInfo.ColorId = 0;\n\n                EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors);\n\n                // IsForceRandomStarts\n                if (pInfo != null && playerExtraOptions.IsForceRandomStarts)\n                    pInfo.StartingLocation = 0;\n\n                EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts);\n            }\n\n            CopyPlayerDataToUI();\n            RefreshBtnPlayerExtraOptionsOpenTexture();\n        }\n\n        private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int playerIndex, bool enable)\n        {\n            var pInfo = GetPlayerInfoForIndex(playerIndex);\n            var allowOtherPlayerOptionsChange = AllowPlayerOptionsChange() && pInfo != null;\n            clientDropDown.AllowDropDown = enable && (allowOtherPlayerOptionsChange || pInfo?.Name == ProgramConstants.PLAYERNAME);\n        }\n\n        protected PlayerInfo GetPlayerInfoForIndex(int playerIndex)\n        {\n            if (playerIndex < Players.Count)\n                return Players[playerIndex];\n\n            if (playerIndex < Players.Count + AIPlayers.Count)\n                return AIPlayers[playerIndex - Players.Count];\n\n            return null;\n        }\n\n        protected PlayerExtraOptions GetPlayerExtraOptions() =>\n            PlayerExtraOptionsPanel == null ? new PlayerExtraOptions() : PlayerExtraOptionsPanel.GetPlayerExtraOptions();\n\n        protected void SetPlayerExtraOptions(PlayerExtraOptions playerExtraOptions) => PlayerExtraOptionsPanel?.SetPlayerExtraOptions(playerExtraOptions);\n\n        protected string GetTeamMappingsError() => GetPlayerExtraOptions()?.GetTeamMappingsError();\n\n        private Texture2D LoadTextureOrNull(string name) =>\n            AssetLoader.AssetExists(name) ? AssetLoader.LoadTexture(name) : null;\n\n        /// <summary>\n        /// Loads random side selectors from GameOptions.ini.\n        /// </summary>\n        /// <param name=\"selectorNames\">The UI names of random selectors.</param>\n        /// <param name=\"selectorSides\">The side IDs to choose from for the selectors.</param>\n        private void GetRandomSelectors(List<string> selectorNames, List<int[]> selectorSides)\n        {\n            List<string> keys = GameOptionsIni.GetSectionKeys(\"RandomSelectors\");\n\n            if (keys == null)\n                return;\n\n            foreach (string randomSelector in keys)\n            {\n                List<int> randomSides = new List<int>();\n                try\n                {\n                    string[] tmp = GameOptionsIni.GetStringListValue(\"RandomSelectors\", randomSelector, string.Empty);\n                    randomSides = Array.ConvertAll(tmp, int.Parse).ToList();\n                    randomSides.RemoveAll(x => (x >= SideCount || x < 0));\n                }\n                catch (FormatException) { }\n\n                if (randomSides.Count > 1)\n                {\n                    selectorNames.Add(randomSelector);\n                    selectorSides.Add(randomSides.ToArray());\n                }\n            }\n        }\n\n        protected abstract void BtnLaunchGame_LeftClick(object sender, EventArgs e);\n\n        protected abstract void BtnLeaveGame_LeftClick(object sender, EventArgs e);\n\n        /// <summary>\n        /// Updates Discord Rich Presence with actual information.\n        /// </summary>\n        /// <param name=\"resetTimer\">Whether to restart the \"Elapsed\" timer or not</param>\n        protected abstract void UpdateDiscordPresence(bool resetTimer = false);\n\n        /// <summary>\n        /// Resets Discord Rich Presence to default state.\n        /// </summary>\n        protected void ResetDiscordPresence() => discordHandler.UpdatePresence();\n\n        protected void LoadDefaultGameModeMap()\n        {\n            if (ddGameModeMapFilter.Items.Count > 0)\n            {\n                ddGameModeMapFilter.SelectedIndex = GetDefaultGameModeMapFilterIndex();\n\n                lbGameModeMapList.SelectedIndex = 0;\n            }\n        }\n\n        protected int GetDefaultGameModeMapFilterIndex()\n        {\n            int firstNonEmptyFilter = ddGameModeMapFilter.Items.FindIndex(i => (i.Tag as GameModeMapFilter)?.Any() ?? false);\n            if (firstNonEmptyFilter == -1)\n                firstNonEmptyFilter = 0;\n\n            return firstNonEmptyFilter;\n        }\n\n        protected GameModeMapFilter GetDefaultGameModeMapFilter()\n        {\n            return ddGameModeMapFilter.Items[GetDefaultGameModeMapFilterIndex()].Tag as GameModeMapFilter;\n        }\n\n        private int GetSpectatorSideIndex() => SideCount + RandomSelectorCount;\n\n        /// <summary>\n        /// Applies disallowed side indexes to the side option drop-downs\n        /// and player options for human or computer players.\n        /// </summary>\n        protected void CheckDisallowedSidesForGroup(bool forHumanPlayers)\n        {\n            var disallowedSideArray = GetDisallowedSidesForGroup(forHumanPlayers);\n            var playerInfos = forHumanPlayers ? Players : AIPlayers;\n            int defaultSide = 0;\n            int allowedSideCount = disallowedSideArray.Count(b => b == false);\n\n            if (allowedSideCount == 1)\n            {\n                // Disallow Random\n\n                for (int i = 0; i < disallowedSideArray.Length; i++)\n                {\n                    if (!disallowedSideArray[i])\n                        defaultSide = i + RandomSelectorCount;\n                }\n\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    var dd = ddPlayerSides[pInfo.Index];\n                    for (int i = 0; i < RandomSelectorCount; i++)\n                        dd.Items[i].Selectable = false;\n                }\n            }\n            else\n            {\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    var dd = ddPlayerSides[pInfo.Index];\n                    for (int i = 0; i < RandomSelectorCount; i++)\n                        dd.Items[i].Selectable = true;\n                }\n            }\n\n            // Disable custom random groups if all or all except one of included sides are unavailable.\n            int c = 0;\n            foreach (int[] randomSides in RandomSelectors)\n            {\n                int disableCount = 0;\n\n                foreach (int side in randomSides)\n                {\n                    if (disallowedSideArray[side])\n                        disableCount++;\n                }\n\n                bool disabled = disableCount >= randomSides.Length - 1;\n\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    var dd = ddPlayerSides[pInfo.Index];\n                    dd.Items[1 + c].Selectable = !disabled;\n\n                    if (pInfo.SideId == 1 + c && disabled)\n                        pInfo.SideId = defaultSide;\n                }\n\n                c++;\n            }\n\n            // Go over the side array and either disable or enable the side\n            // dropdown options depending on whether the side is available\n            for (int i = 0; i < disallowedSideArray.Length; i++)\n            {\n                bool disabled = disallowedSideArray[i];\n\n                if (disabled)\n                {\n                    // Change the sides of players that use the disabled\n                    // side to the default side\n                    foreach (PlayerInfo pInfo in playerInfos)\n                    {\n                        var dd = ddPlayerSides[pInfo.Index];\n                        dd.Items[i + RandomSelectorCount].Selectable = false;\n\n                        if (pInfo.SideId == i + RandomSelectorCount)\n                            pInfo.SideId = defaultSide;\n                    }\n                }\n                else\n                {\n                    foreach (PlayerInfo pInfo in playerInfos)\n                    {\n                        var dd = ddPlayerSides[pInfo.Index];\n                        dd.Items[i + RandomSelectorCount].Selectable = true;\n                    }\n                }\n            }\n\n            // If only 1 side is allowed, change all players' sides to that\n            if (allowedSideCount == 1)\n            {\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    if (pInfo.SideId == 0)\n                        pInfo.SideId = defaultSide;\n                }\n            }\n\n            if (GameModeMap != null && GameModeMap.CoopInfo != null)\n            {\n                // Disallow spectator\n\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    if (pInfo.SideId == GetSpectatorSideIndex())\n                        pInfo.SideId = defaultSide;\n                }\n\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    var dd = ddPlayerSides[pInfo.Index];\n                    if (dd.Items.Count > GetSpectatorSideIndex())\n                        dd.Items[SideCount + RandomSelectorCount].Selectable = false;\n                }\n            }\n            else\n            {\n                foreach (PlayerInfo pInfo in playerInfos)\n                {\n                    var dd = ddPlayerSides[pInfo.Index];\n                    if (dd.Items.Count > SideCount + RandomSelectorCount)\n                        dd.Items[SideCount + RandomSelectorCount].Selectable = true;\n                }\n            }\n        }\n\n        /// <summary>\n        /// Applies disallowed side indexes to the side option drop-downs\n        /// and player options.\n        /// </summary>\n        protected void CheckDisallowedSides()\n        {\n            CheckDisallowedSidesForGroup(forHumanPlayers: false);\n            CheckDisallowedSidesForGroup(forHumanPlayers: true);\n        }\n\n        /// <summary>\n        /// Gets a list of side indexes that are disallowed for human or computer players.\n        /// </summary>\n        /// <returns>A list of disallowed side indexes.</returns>\n        protected bool[] GetDisallowedSidesForGroup(bool forHumanPlayers)\n        {\n            var returnValue = GetDisallowedSides();\n            var sides = forHumanPlayers ? GameMode?.DisallowedHumanPlayerSides : GameMode?.DisallowedComputerPlayerSides;\n            if (sides != null)\n            {\n                foreach (int i in sides)\n                    returnValue[i] = true;\n            }\n\n            return returnValue;\n        }\n\n        /// <summary>\n        /// Gets a list of side indexes that are disallowed.\n        /// </summary>\n        /// <returns>A list of disallowed side indexes.</returns>\n        protected bool[] GetDisallowedSides()\n        {\n            var returnValue = new bool[SideCount];\n\n            if (GameModeMap != null && GameModeMap.CoopInfo != null)\n            {\n                // Co-Op map disallowed side logic\n\n                foreach (int disallowedSideIndex in GameModeMap.CoopInfo.DisallowedPlayerSides)\n                    returnValue[disallowedSideIndex] = true;\n            }\n\n            if (GameMode != null)\n            {\n                foreach (int disallowedSideIndex in GameMode.DisallowedPlayerSides)\n                    returnValue[disallowedSideIndex] = true;\n            }\n\n            foreach (var checkBox in CheckBoxes)\n                checkBox.ApplyDisallowedSideIndex(returnValue);\n\n            return returnValue;\n        }\n\n        /// <summary>\n        /// Randomizes options of both human and AI players\n        /// and returns the options as an array of PlayerHouseInfos.\n        /// </summary>\n        /// <returns>An array of PlayerHouseInfos.</returns>\n        protected virtual PlayerHouseInfo[] Randomize(List<TeamStartMapping> teamStartMappings, Random pseudoRandom)\n        {\n            int totalPlayerCount = Players.Count + AIPlayers.Count;\n            PlayerHouseInfo[] houseInfos = new PlayerHouseInfo[totalPlayerCount];\n\n            for (int i = 0; i < totalPlayerCount; i++)\n                houseInfos[i] = new PlayerHouseInfo();\n\n            // Gather list of spectators\n            for (int i = 0; i < Players.Count; i++)\n                houseInfos[i].IsSpectator = Players[i].SideId == GetSpectatorSideIndex();\n\n            // Gather list of available colors\n\n            List<int> freeColors = new List<int>();\n\n            for (int cId = 0; cId < MPColors.Count; cId++)\n                freeColors.Add(cId);\n\n            if (GameModeMap.CoopInfo != null)\n            {\n                foreach (int colorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors)\n                    freeColors.Remove(colorIndex);\n            }\n\n            foreach (PlayerInfo player in Players)\n                freeColors.Remove(player.ColorId - 1); // The first color is Random\n\n            foreach (PlayerInfo aiPlayer in AIPlayers)\n                freeColors.Remove(aiPlayer.ColorId - 1);\n\n            // Gather list of available starting locations\n\n            List<int> freeStartingLocations = new List<int>();\n            List<int> takenStartingLocations = new List<int>();\n\n            foreach (int i in GameModeMap.AllowedStartingLocations)\n                freeStartingLocations.Add(i - 1);\n\n            for (int i = 0; i < Players.Count; i++)\n            {\n                if (!houseInfos[i].IsSpectator)\n                {\n                    freeStartingLocations.Remove(Players[i].StartingLocation - 1);\n                    //takenStartingLocations.Add(Players[i].StartingLocation - 1);\n                    // ^ Gives everyone with a selected location a completely random\n                    // location in-game, because PlayerHouseInfo.RandomizeStart already\n                    // fills the list itself\n                }\n            }\n\n            for (int i = 0; i < AIPlayers.Count; i++)\n                freeStartingLocations.Remove(AIPlayers[i].StartingLocation - 1);\n\n            foreach (var teamStartMapping in teamStartMappings.Where(mapping => mapping.IsBlock))\n                freeStartingLocations.Remove(teamStartMapping.StartingWaypoint);\n\n            // Randomize options\n\n            for (int i = 0; i < totalPlayerCount; i++)\n            {\n                PlayerInfo pInfo;\n                PlayerHouseInfo pHouseInfo = houseInfos[i];\n                bool[] disallowedSides;\n\n                if (i < Players.Count)\n                {\n                    pInfo = Players[i];\n                    disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: true);\n                }\n                else\n                {\n                    pInfo = AIPlayers[i - Players.Count];\n                    disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: false);\n                }\n\n                pHouseInfo.RandomizeSide(pInfo, SideCount, pseudoRandom, disallowedSides, RandomSelectors, RandomSelectorCount);\n\n                pHouseInfo.RandomizeColor(pInfo, freeColors, MPColors, pseudoRandom);\n\n                bool overrideGameRandomLocations = teamStartMappings.Any()\n                    || GameModeMap.AllowedStartingLocations.Max() > GameModeMap.MaxPlayers; // non-sequential AllowedStartingLocations\n                pHouseInfo.RandomizeStart(pInfo, pseudoRandom, freeStartingLocations, takenStartingLocations, overrideGameRandomLocations);\n            }\n\n            return houseInfos;\n        }\n\n        /// <summary>\n        /// Writes spawn.ini. Returns the player house info returned from the randomizer.\n        /// </summary>\n        private PlayerHouseInfo[] WriteSpawnIni(Random pseudoRandom)\n        {\n            Logger.Log(\"Writing spawn.ini\");\n\n            FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS);\n\n            spawnerSettingsFile.Delete();\n\n            if (GameModeMap.IsCoop)\n            {\n                foreach (PlayerInfo pInfo in Players)\n                {\n                    Debug.Assert(pInfo.TeamId == 1, \"Co-ops should always set TeamId to 1 before lanching the game\");\n                    pInfo.TeamId = 1;\n                }\n\n                foreach (PlayerInfo pInfo in AIPlayers)\n                {\n                    Debug.Assert(pInfo.TeamId == 1, \"Co-ops should always set TeamId to 1 before lanching the game\");\n                    pInfo.TeamId = 1;\n                }\n            }\n\n            var teamStartMappings = new List<TeamStartMapping>(0);\n            if (PlayerExtraOptionsPanel != null)\n            {\n                teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings();\n            }\n\n            PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, pseudoRandom);\n\n            IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName);\n\n            IniSection settings = new IniSection(\"Settings\");\n\n            settings.SetStringValue(\"Name\", ProgramConstants.PLAYERNAME);\n            settings.SetStringValue(\"Scenario\", ProgramConstants.SPAWNMAP_INI);\n            settings.SetStringValue(\"UIGameMode\", GameMode.UntranslatedUIName);\n            settings.SetStringValue(\"UIMapName\", Map.UntranslatedName);\n\n            // needed for translation in game loading lobbies\n            if (Map.Official)\n                settings.SetStringValue(\"MapID\", Map.BaseFilePath);\n\n            settings.SetIntValue(\"PlayerCount\", Players.Count);\n            int myIndex = Players.FindIndex(c => c.Name == ProgramConstants.PLAYERNAME);\n            settings.SetIntValue(\"Side\", houseInfos[myIndex].InternalSideIndex);\n            settings.SetBooleanValue(\"IsSpectator\", houseInfos[myIndex].IsSpectator);\n            settings.SetIntValue(\"Color\", houseInfos[myIndex].ColorIndex);\n            settings.SetStringValue(\"CustomLoadScreen\", LoadingScreenController.GetLoadScreenName(houseInfos[myIndex].InternalSideIndex.ToString()));\n            settings.SetIntValue(\"AIPlayers\", AIPlayers.Count);\n            settings.SetIntValue(\"Seed\", RandomSeed);\n            if (GetPvPTeamCount() > 1)\n                settings.SetBooleanValue(\"CoachMode\", true);\n            if (GetGameType() == GameType.Coop)\n                settings.SetBooleanValue(\"AutoSurrender\", false);\n            spawnIni.AddSection(settings);\n            WriteSpawnIniAdditions(spawnIni);\n\n            foreach (GameLobbyCheckBox chkBox in CheckBoxes)\n                chkBox.ApplySpawnIniCode(spawnIni);\n\n            foreach (GameLobbyDropDown dd in DropDowns)\n                dd.ApplySpawnIniCode(spawnIni);\n\n            // Apply forced options from GameOptions.ini\n\n            List<string> forcedKeys = GameOptionsIni.GetSectionKeys(\"ForcedSpawnIniOptions\");\n\n            if (forcedKeys != null)\n            {\n                foreach (string key in forcedKeys)\n                {\n                    spawnIni.SetStringValue(\"Settings\", key,\n                        GameOptionsIni.GetStringValue(\"ForcedSpawnIniOptions\", key, String.Empty));\n                }\n            }\n\n            GameMode.ApplySpawnIniCode(spawnIni); // Forced options from the game mode\n            Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count,\n                AIPlayers.Count, GameModeMap.IsCoop, GameModeMap.CoopInfo, GameModeMap.CoopDifficultyLevel, pseudoRandom, SideCount); // Forced options from the map\n\n            // Player options\n\n            int otherId = 1;\n\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                PlayerInfo pInfo = Players[pId];\n                PlayerHouseInfo pHouseInfo = houseInfos[pId];\n\n                if (pInfo.Name == ProgramConstants.PLAYERNAME)\n                    continue;\n\n                string sectionName = \"Other\" + otherId;\n\n                spawnIni.SetStringValue(sectionName, \"Name\", pInfo.Name);\n                spawnIni.SetIntValue(sectionName, \"Side\", pHouseInfo.InternalSideIndex);\n                spawnIni.SetBooleanValue(sectionName, \"IsSpectator\", pHouseInfo.IsSpectator);\n                spawnIni.SetIntValue(sectionName, \"Color\", pHouseInfo.ColorIndex);\n                spawnIni.SetStringValue(sectionName, \"Ip\", GetIPAddressForPlayer(pInfo));\n                spawnIni.SetIntValue(sectionName, \"Port\", pInfo.Port);\n\n                otherId++;\n            }\n\n            // The spawner assigns players to SpawnX houses based on their in-game color index\n            List<int> multiCmbIndexes = new List<int>();\n            var sortedColorList = MPColors.OrderBy(mpc => mpc.GameColorIndex).ToList();\n\n            for (int cId = 0; cId < sortedColorList.Count; cId++)\n            {\n                for (int pId = 0; pId < Players.Count; pId++)\n                {\n                    if (houseInfos[pId].ColorIndex == sortedColorList[cId].GameColorIndex)\n                        multiCmbIndexes.Add(pId);\n                }\n            }\n\n            if (AIPlayers.Count > 0)\n            {\n                for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n                {\n                    int multiId = multiCmbIndexes.Count + aiId + 1;\n\n                    string keyName = \"Multi\" + multiId;\n\n                    spawnIni.SetIntValue(\"HouseHandicaps\", keyName, AIPlayers[aiId].HouseHandicapAILevel);\n                    spawnIni.SetIntValue(\"HouseCountries\", keyName, houseInfos[Players.Count + aiId].InternalSideIndex);\n                    spawnIni.SetIntValue(\"HouseColors\", keyName, houseInfos[Players.Count + aiId].ColorIndex);\n                }\n            }\n\n            for (int multiId = 0; multiId < multiCmbIndexes.Count; multiId++)\n            {\n                int pIndex = multiCmbIndexes[multiId];\n                if (houseInfos[pIndex].IsSpectator)\n                    spawnIni.SetBooleanValue(\"IsSpectator\", \"Multi\" + (multiId + 1), true);\n            }\n\n            // Write alliances, the code is pretty big so let's take it to another class\n            AllianceHolder.WriteInfoToSpawnIni(Players, AIPlayers, multiCmbIndexes, houseInfos.ToList(), teamStartMappings, spawnIni);\n\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                int startingWaypoint = houseInfos[multiCmbIndexes[pId]].StartingWaypoint;\n\n                // -1 means no starting location at all - let the game itself pick the starting location\n                // using its own logic\n                if (startingWaypoint > -1)\n                {\n                    int multiIndex = pId + 1;\n                    spawnIni.SetIntValue(\"SpawnLocations\", \"Multi\" + multiIndex,\n                        startingWaypoint);\n                }\n            }\n\n            for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n            {\n                int startingWaypoint = houseInfos[Players.Count + aiId].StartingWaypoint;\n\n                if (startingWaypoint > -1)\n                {\n                    int multiIndex = Players.Count + aiId + 1;\n                    spawnIni.SetIntValue(\"SpawnLocations\", \"Multi\" + multiIndex,\n                        startingWaypoint);\n                }\n            }\n\n            spawnIni.WriteIniFile();\n\n            return houseInfos;\n        }\n\n        /// <summary>\n        /// Returns the number of teams with human players in them.\n        /// Does not count spectators and human players that don't have a team set.\n        /// </summary>\n        /// <returns>The number of human player teams in the game.</returns>\n        private int GetPvPTeamCount()\n        {\n            int[] teamPlayerCounts = new int[4];\n            int playerTeamCount = 0;\n\n            foreach (PlayerInfo pInfo in Players)\n            {\n                if (pInfo.IsAI || IsPlayerSpectator(pInfo))\n                    continue;\n\n                if (pInfo.TeamId > 0)\n                {\n                    teamPlayerCounts[pInfo.TeamId - 1]++;\n                    if (teamPlayerCounts[pInfo.TeamId - 1] == 2)\n                        playerTeamCount++;\n                }\n            }\n\n            return playerTeamCount;\n        }\n\n        /// <summary>\n        /// Checks whether the specified player has selected Spectator as their side.\n        /// </summary>\n        /// <param name=\"pInfo\">The player.</param>\n        /// <returns>True if the player is a spectator, otherwise false.</returns>\n        protected bool IsPlayerSpectator(PlayerInfo pInfo)\n        {\n            if (pInfo.SideId == GetSpectatorSideIndex())\n                return true;\n\n            return false;\n        }\n\n        protected virtual string GetIPAddressForPlayer(PlayerInfo player) => \"0.0.0.0\";\n\n        /// <summary>\n        /// Override this in a derived class to write game lobby specific code to\n        /// spawn.ini. For example, CnCNet game lobbies should write tunnel info\n        /// in this method.\n        /// </summary>\n        /// <param name=\"iniFile\">The spawn INI file.</param>\n        protected virtual void WriteSpawnIniAdditions(IniFile iniFile)\n        {\n            // Do nothing by default\n        }\n\n        private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos)\n        {\n            matchStatistics = new MatchStatistics(ProgramConstants.GAME_VERSION, UniqueGameID,\n                Map.UntranslatedName, GameMode.UntranslatedUIName, Players.Count, GameModeMap.IsCoop);\n\n            bool isValidForStar = true;\n            foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n            {\n                if (!checkBox.AllowScoring)\n                {\n                    isValidForStar = false;\n                    break;\n                }\n            }\n            foreach (GameLobbyDropDown dropDown in DropDowns)\n            {\n                if (!dropDown.AllowScoring)\n                {\n                    isValidForStar = false;\n                    break;\n                }\n            }\n\n            matchStatistics.IsValidForStar = isValidForStar;\n\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                PlayerInfo pInfo = Players[pId];\n                matchStatistics.AddPlayer(pInfo.Name, pInfo.Name == ProgramConstants.PLAYERNAME,\n                    false, pInfo.SideId == SideCount + RandomSelectorCount, houseInfos[pId].SideIndex + 1, pInfo.TeamId,\n                    MPColors.FindIndex(c => c.GameColorIndex == houseInfos[pId].ColorIndex), 10);\n            }\n\n            for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n            {\n                var pHouseInfo = houseInfos[Players.Count + aiId];\n                PlayerInfo aiInfo = AIPlayers[aiId];\n                matchStatistics.AddPlayer(\"Computer\", false, true, false,\n                    pHouseInfo.SideIndex + 1, aiInfo.TeamId,\n                    MPColors.FindIndex(c => c.GameColorIndex == pHouseInfo.ColorIndex),\n                    aiInfo.AILevel);\n            }\n        }\n\n        /// <summary>\n        /// Writes spawnmap.ini.\n        /// </summary>\n        private void WriteMap(PlayerHouseInfo[] houseInfos, Random pseudoRandom)\n        {\n            FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI);\n\n            DeleteSupplementalMapFiles();\n            spawnMapIniFile.Delete();\n\n            Logger.Log(\"Writing map.\");\n\n            Logger.Log(\"Loading map INI from \" + Map.CompleteFilePath);\n\n            IniFile mapIni = Map.GetMapIni();\n\n            IniFile globalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", \"Map Code\", \"GlobalCode.ini\"));\n\n            foreach (IniFile iniFile in GameMode.GetMapRulesIniFiles(pseudoRandom))\n                MapCodeHelper.ApplyMapCode(mapIni, iniFile);\n\n            MapCodeHelper.ApplyMapCode(mapIni, globalCodeIni);\n\n            if (isMultiplayer)\n            {\n                IniFile mpGlobalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", \"Map Code\", \"MultiplayerGlobalCode.ini\"));\n                MapCodeHelper.ApplyMapCode(mapIni, mpGlobalCodeIni);\n            }\n            else\n            {\n                // 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.\n                // This inconsistency can result in differing spawnmap.ini files among players, causing desyncs in CnCNet YR games.\n                // Theoretically it can be useful for some singleplayer campaign tracking\n                // But it isn't currently used by any CnCNet game or mod\n                // The code below only applies to the single player case\n                string mapIniFileName = Path.GetFileName(mapIni.FileName);\n                mapIni.SetStringValue(\"Basic\", \"OriginalFilename\", mapIniFileName);\n            }\n\n            foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n                checkBox.ApplyMapCode(mapIni, GameMode);\n\n            foreach (GameLobbyDropDown dropDown in DropDowns)\n                dropDown.ApplyMapCode(mapIni, GameMode);\n\n            mapIni.MoveSectionToFirst(\"MultiplayerDialogSettings\"); // Required by YR\n\n            CopySupplementalMapFiles(mapIni);\n\n            ManipulateStartingLocations(mapIni, houseInfos);\n\n            mapIni.WriteIniFile(spawnMapIniFile.FullName);\n        }\n\n        /// <summary>\n        /// Some mods require that .map files also have supplemental files copied over with the spawnmap.ini.\n        /// \n        /// This function scans the directory containing the map file and looks for other files with the\n        /// same base filename as the map file that are allowed by the client configuration.\n        /// Those files are then copied to the game base path with the base filename of \"spawnmap.EXT\".\n        /// </summary>\n        /// <param name=\"mapIni\"></param>\n        private void CopySupplementalMapFiles(IniFile mapIni)\n        {\n            var mapFileInfo = new FileInfo(mapIni.FileName);\n            string mapFileBaseName = Path.GetFileNameWithoutExtension(mapFileInfo.Name);\n\n            IEnumerable<string> supplementalMapFiles = GetSupplementalMapFiles(mapFileInfo.DirectoryName, mapFileBaseName).ToList();\n            if (!supplementalMapFiles.Any())\n                return;\n\n            List<string> supplementalFileNames = new();\n            foreach (string file in supplementalMapFiles)\n            {\n                try\n                {\n                    // Copy each supplemental file\n                    string supplementalFileName = $\"spawnmap{Path.GetExtension(file)}\";\n                    File.Copy(file, SafePath.CombineFilePath(ProgramConstants.GamePath, supplementalFileName), true);\n                    supplementalFileNames.Add(supplementalFileName);\n                }\n                catch (Exception ex)\n                {\n                    string errorMessage = \"Unable to copy supplemental map file\".L10N(\"Client:Main:SupplementalFileCopyError\") + $\" {file}\";\n                    Logger.Log(errorMessage);\n                    Logger.Log(ex.ToString());\n                    XNAMessageBox.Show(WindowManager, \"Error\".L10N(\"Client:Main:Error\"), errorMessage);\n\n                }\n            }\n\n            // Write the supplemental map files to the INI (eventual spawnmap.ini)\n            mapIni.SetStringValue(\"Basic\", \"SupplementalFiles\", string.Join(\",\", supplementalFileNames));\n        }\n\n        /// <summary>\n        /// Delete all supplemental map files from last spawn\n        /// </summary>\n        private void DeleteSupplementalMapFiles()\n        {\n            IEnumerable<string> supplementalMapFilePaths = GetSupplementalMapFiles(ProgramConstants.GamePath, \"spawnmap\").ToList();\n            if (!supplementalMapFilePaths.Any())\n                return;\n\n            foreach (string supplementalMapFilename in supplementalMapFilePaths)\n            {\n                try\n                {\n                    File.Delete(supplementalMapFilename);\n                }\n                catch (Exception ex)\n                {\n                    string errorMessage = \"Unable to delete supplemental map file\".L10N(\"Client:Main:SupplementalFileDeleteError\") + $\" {supplementalMapFilename}\";\n                    Logger.Log(errorMessage);\n                    Logger.Log(ex.ToString());\n                    XNAMessageBox.Show(WindowManager, \"Error\".L10N(\"Client:Main:Error\"), errorMessage);\n                }\n            }\n        }\n\n        private static IEnumerable<string> GetSupplementalMapFiles(string basePath, string baseFileName)\n        {\n            // Get the supplemental file names for allowable extensions\n            var supplementalMapFileNames = ClientConfiguration.Instance.SupplementalMapFileExtensions\n                .Select(ext => $\"{baseFileName}.{ext}\")\n                .ToList();\n\n            if (!supplementalMapFileNames.Any())\n                return new List<string>();\n\n            // Get full file paths for all possible supplemental files\n            return Directory.GetFiles(basePath, $\"{baseFileName}.*\")\n                .Where(f => supplementalMapFileNames.Contains(Path.GetFileName(f)));\n        }\n\n        private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] houseInfos)\n        {\n            if (RemoveStartingLocations)\n            {\n                if (GameModeMap.EnforceMaxPlayers)\n                    return;\n\n                // All random starting locations given by the game\n                IniSection waypointSection = mapIni.GetSection(\"Waypoints\");\n                if (waypointSection == null)\n                    return;\n\n                // TODO implement IniSection.RemoveKey in Rampastring.Tools, then\n                // remove implementation that depends on internal implementation\n                // of IniSection\n                for (int i = 0; i <= 7; i++)\n                {\n                    int index = waypointSection.Keys.FindIndex(k => !string.IsNullOrEmpty(k.Key) && k.Key == i.ToString());\n                    if (index > -1)\n                        waypointSection.Keys.RemoveAt(index);\n                }\n            }\n\n            // Multiple players cannot properly share the same starting location\n            // without breaking the SpawnX house logic that pre-placed objects depend on\n\n            // To work around this, we add new starting locations that just point\n            // to the same cell coordinates as existing stacked starting locations\n            // and make additional players in the same start loc start from the new\n            // starting locations instead.\n\n            // As an additional restriction, players can only start from waypoints 0 to 7.\n            // That means that if the map already has too many starting waypoints,\n            // we need to move existing (but un-occupied) starting waypoints to point\n            // to the stacked locations so we can spawn the players there.\n\n\n            // Check for stacked starting locations (locations with more than 1 player on it)\n            bool[] startingLocationUsed = new bool[MAX_PLAYER_COUNT];\n            bool stackedStartingLocations = false;\n            foreach (PlayerHouseInfo houseInfo in houseInfos)\n            {\n                if (houseInfo.RealStartingWaypoint > -1)\n                {\n                    startingLocationUsed[houseInfo.RealStartingWaypoint] = true;\n\n                    // If assigned starting waypoint is unknown while the real\n                    // starting location is known, it means that\n                    // the location is shared with another player\n                    if (houseInfo.StartingWaypoint == -1)\n                    {\n                        stackedStartingLocations = true;\n                    }\n                }\n            }\n\n            // If any starting location is stacked, re-arrange all starting locations\n            // so that unused starting locations are removed and made to point at used\n            // starting locations\n            if (!stackedStartingLocations)\n                return;\n\n            // We also need to modify spawn.ini because WriteSpawnIni\n            // doesn't handle stacked positions.\n            // We could move this code there, but then we'd have to process\n            // the stacked locations in two places (here and in WriteSpawnIni)\n            // because we'd need to modify the map anyway.\n            // Not sure whether having it like this or in WriteSpawnIni\n            // is better, but this implementation is quicker to write for now.\n            IniFile spawnIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS));\n\n            // For each player, check if they're sharing the starting location\n            // with someone else\n            // If they are, find an unused waypoint and assign their\n            // starting location to match that\n            for (int pId = 0; pId < houseInfos.Length; pId++)\n            {\n                PlayerHouseInfo houseInfo = houseInfos[pId];\n\n                if (houseInfo.RealStartingWaypoint > -1 &&\n                    houseInfo.StartingWaypoint == -1)\n                {\n                    // Find first unused starting location index\n                    int unusedLocation = -1;\n                    for (int i = 0; i < startingLocationUsed.Length; i++)\n                    {\n                        if (!startingLocationUsed[i])\n                        {\n                            unusedLocation = i;\n                            startingLocationUsed[i] = true;\n                            break;\n                        }\n                    }\n\n                    houseInfo.StartingWaypoint = unusedLocation;\n                    mapIni.SetIntValue(\"Waypoints\", unusedLocation.ToString(),\n                        mapIni.GetIntValue(\"Waypoints\", houseInfo.RealStartingWaypoint.ToString(), 0));\n                    spawnIni.SetIntValue(\"SpawnLocations\", $\"Multi{pId + 1}\", unusedLocation);\n                }\n            }\n\n            spawnIni.WriteIniFile();\n        }\n\n        /// <summary>\n        /// Writes spawn.ini, writes the map file, initializes statistics and\n        /// starts the game process.\n        /// </summary>\n        protected virtual void StartGame()\n        {\n            Random pseudoRandom = new Random(RandomSeed);\n\n            PlayerHouseInfo[] houseInfos = WriteSpawnIni(pseudoRandom);\n            InitializeMatchStatistics(houseInfos);\n            WriteMap(houseInfos, pseudoRandom);\n\n            GameProcessLogic.GameProcessExited += GameProcessExited_Callback;\n\n            GameProcessLogic.StartGameProcess(WindowManager);\n            UpdateDiscordPresence(true);\n        }\n\n        private void GameProcessExited_Callback() => AddCallback(new Action(GameProcessExited), null);\n\n        protected virtual void GameProcessExited()\n        {\n            GameProcessLogic.GameProcessExited -= GameProcessExited_Callback;\n\n            Logger.Log(\"GameProcessExited: Parsing statistics.\");\n\n            matchStatistics?.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false);\n\n            Logger.Log(\"GameProcessExited: Adding match to statistics.\");\n\n            StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics);\n\n            ClearReadyStatuses();\n\n            CopyPlayerDataToUI();\n\n            UpdateDiscordPresence(true);\n        }\n\n        /// <summary>\n        /// \"Copies\" player information from the UI to internal memory,\n        /// applying users' player options changes.\n        /// </summary>\n        protected virtual void CopyPlayerDataFromUI(object sender, EventArgs e)\n        {\n            if (PlayerUpdatingInProgress)\n                return;\n\n            var senderDropDown = (XNADropDown)sender;\n            if ((bool)senderDropDown.Tag)\n                ClearReadyStatuses();\n\n            var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId;\n\n            if (Players.Count > MAX_PLAYER_COUNT)\n                throw new Exception($\"Player count exceeds maximum of {MAX_PLAYER_COUNT}. How could this happen?\");\n\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                PlayerInfo pInfo = Players[pId];\n\n                pInfo.ColorId = ddPlayerColors[pId].SelectedIndex;\n                pInfo.SideId = ddPlayerSides[pId].SelectedIndex;\n                pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex;\n                pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex;\n\n                if (pInfo.SideId == SideCount + RandomSelectorCount)\n                    pInfo.StartingLocation = 0;\n\n                XNADropDown ddName = ddPlayerNames[pId];\n\n                switch (ddName.SelectedIndex)\n                {\n                    case 0:\n                        break;\n                    case 1:\n                        ddName.SelectedIndex = 0;\n                        break;\n                    case 2:\n                        KickPlayer(pId);\n                        break;\n                    case 3:\n                        BanPlayer(pId);\n                        break;\n                }\n            }\n\n            AIPlayers.Clear();\n            for (int cmbId = Players.Count; cmbId < MAX_PLAYER_COUNT; cmbId++)\n            {\n                XNADropDown dd = ddPlayerNames[cmbId];\n                dd.Items[0].Text = \"-\";\n\n                if (dd.SelectedIndex < 1)\n                    continue;\n\n                PlayerInfo aiPlayer = new PlayerInfo\n                {\n                    Name = dd.Items[dd.SelectedIndex].Text,\n                    AILevel = dd.SelectedIndex - 1,\n                    SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0),\n                    ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0),\n                    StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0),\n                    TeamId = Map != null && GameModeMap.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0),\n                    IsAI = true\n                };\n\n                AIPlayers.Add(aiPlayer);\n            }\n\n            CopyPlayerDataToUI();\n            btnLaunchGame.SetRank(GetRank());\n\n            if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId)\n                UpdateDiscordPresence();\n        }\n\n        /// <summary>\n        /// Sets the ready status of all non-host human players to false.\n        /// </summary>\n        /// <param name=\"resetAutoReady\">If set, players with autoready enabled are reset as well.</param>\n        protected void ClearReadyStatuses(bool resetAutoReady = false)\n        {\n            for (int i = 1; i < Players.Count; i++)\n            {\n                if (resetAutoReady || !Players[i].AutoReady || Players[i].IsInGame)\n                    Players[i].Ready = false;\n            }\n        }\n\n        private bool CanRightClickMultiplayer(XNADropDownItem selectedPlayer)\n        {\n            return selectedPlayer != null &&\n                   selectedPlayer.Text != ProgramConstants.PLAYERNAME &&\n                   !ProgramConstants.AI_PLAYER_NAMES.Contains(selectedPlayer.Text);\n        }\n\n        private void MultiplayerName_RightClick(object sender, EventArgs e)\n        {\n            var selectedPlayer = ((XNADropDown)sender).SelectedItem;\n            if (!CanRightClickMultiplayer(selectedPlayer))\n                return;\n\n            if (selectedPlayer == null ||\n                selectedPlayer.Text == ProgramConstants.PLAYERNAME)\n            {\n                return;\n            }\n\n            MultiplayerNameRightClicked?.Invoke(this, new MultiplayerNameRightClickedEventArgs(selectedPlayer.Text));\n        }\n\n        /// <summary>\n        /// Applies player information changes done in memory to the UI.\n        /// </summary>\n        protected virtual void CopyPlayerDataToUI()\n        {\n            PlayerUpdatingInProgress = true;\n\n            bool allowOptionsChange = AllowPlayerOptionsChange();\n            var playerExtraOptions = GetPlayerExtraOptions();\n\n            if (Players.Count > MAX_PLAYER_COUNT)\n                throw new Exception($\"Player count exceeds maximum of {MAX_PLAYER_COUNT}. How could this happen?\");\n\n            // Human players\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                PlayerInfo pInfo = Players[pId];\n\n                pInfo.Index = pId;\n\n                XNADropDown ddPlayerName = ddPlayerNames[pId];\n                ddPlayerName.Items[0].Text = pInfo.Name;\n                ddPlayerName.Items[1].Text = string.Empty;\n                ddPlayerName.Items[2].Text = \"Kick\".L10N(\"Client:Main:Kick\");\n                ddPlayerName.Items[3].Text = \"Ban\".L10N(\"Client:Main:Ban\");\n                ddPlayerName.SelectedIndex = 0;\n                ddPlayerName.AllowDropDown = false;\n\n                bool allowPlayerOptionsChange = allowOptionsChange || pInfo.Name == ProgramConstants.PLAYERNAME;\n\n                ddPlayerSides[pId].SelectedIndex = pInfo.SideId;\n                ddPlayerSides[pId].AllowDropDown = !playerExtraOptions.IsForceRandomSides && allowPlayerOptionsChange;\n\n                ddPlayerColors[pId].SelectedIndex = pInfo.ColorId;\n                ddPlayerColors[pId].AllowDropDown = !playerExtraOptions.IsForceRandomColors && allowPlayerOptionsChange;\n\n                ddPlayerStarts[pId].SelectedIndex = pInfo.StartingLocation;\n\n                ddPlayerTeams[pId].SelectedIndex = pInfo.TeamId;\n                if (GameModeMap != null)\n                {\n                    ddPlayerTeams[pId].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowPlayerOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams;\n                    ddPlayerStarts[pId].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowPlayerOptionsChange && !GameModeMap.ForceRandomStartLocations;\n                }\n            }\n\n            // AI players\n            for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n            {\n                PlayerInfo aiInfo = AIPlayers[aiId];\n\n                int index = Players.Count + aiId;\n\n                aiInfo.Index = index;\n\n                XNADropDown ddPlayerName = ddPlayerNames[index];\n                ddPlayerName.Items[0].Text = \"-\";\n                ddPlayerName.Items[1].Text = ProgramConstants.AI_PLAYER_NAMES[0];\n                ddPlayerName.Items[2].Text = ProgramConstants.AI_PLAYER_NAMES[1];\n                ddPlayerName.Items[3].Text = ProgramConstants.AI_PLAYER_NAMES[2];\n                ddPlayerName.SelectedIndex = 1 + aiInfo.AILevel;\n                ddPlayerName.AllowDropDown = allowOptionsChange;\n\n                ddPlayerSides[index].SelectedIndex = aiInfo.SideId;\n                ddPlayerSides[index].AllowDropDown = !playerExtraOptions.IsForceRandomSides && allowOptionsChange;\n\n                ddPlayerColors[index].SelectedIndex = aiInfo.ColorId;\n                ddPlayerColors[index].AllowDropDown = !playerExtraOptions.IsForceRandomColors && allowOptionsChange;\n\n                ddPlayerStarts[index].SelectedIndex = aiInfo.StartingLocation;\n\n                ddPlayerTeams[index].SelectedIndex = aiInfo.TeamId;\n\n                if (GameModeMap != null)\n                {\n                    ddPlayerTeams[index].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams;\n                    ddPlayerStarts[index].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowOptionsChange && !GameModeMap.ForceRandomStartLocations;\n                }\n            }\n\n            // Unused player slots\n            for (int ddIndex = Players.Count + AIPlayers.Count; ddIndex < MAX_PLAYER_COUNT; ddIndex++)\n            {\n                XNADropDown ddPlayerName = ddPlayerNames[ddIndex];\n                ddPlayerName.AllowDropDown = false;\n                ddPlayerName.Items[0].Text = string.Empty;\n                ddPlayerName.Items[1].Text = ProgramConstants.AI_PLAYER_NAMES[0];\n                ddPlayerName.Items[2].Text = ProgramConstants.AI_PLAYER_NAMES[1];\n                ddPlayerName.Items[3].Text = ProgramConstants.AI_PLAYER_NAMES[2];\n                ddPlayerName.SelectedIndex = 0;\n\n                ddPlayerSides[ddIndex].SelectedIndex = -1;\n                ddPlayerSides[ddIndex].AllowDropDown = false;\n\n                ddPlayerColors[ddIndex].SelectedIndex = -1;\n                ddPlayerColors[ddIndex].AllowDropDown = false;\n\n                ddPlayerStarts[ddIndex].SelectedIndex = -1;\n                ddPlayerStarts[ddIndex].AllowDropDown = false;\n\n                ddPlayerTeams[ddIndex].SelectedIndex = -1;\n                ddPlayerTeams[ddIndex].AllowDropDown = false;\n            }\n\n            if (allowOptionsChange && Players.Count + AIPlayers.Count < MAX_PLAYER_COUNT)\n                ddPlayerNames[Players.Count + AIPlayers.Count].AllowDropDown = true;\n\n            MapPreviewBox.UpdateStartingLocationTexts();\n            UpdateMapPreviewBoxEnabledStatus();\n\n            CheckDisallowedSides();\n\n            PlayerUpdatingInProgress = false;\n        }\n\n        /// <summary>\n        /// Updates the enabled status of starting location selectors\n        /// in the map preview box.\n        /// </summary>\n        protected abstract void UpdateMapPreviewBoxEnabledStatus();\n\n        /// <summary>\n        /// Override this in a derived class to kick players.\n        /// </summary>\n        /// <param name=\"playerIndex\">The index of the player that should be kicked.</param>\n        protected virtual void KickPlayer(int playerIndex)\n        {\n            // Do nothing by default\n        }\n\n        /// <summary>\n        /// Override this in a derived class to ban players.\n        /// </summary>\n        /// <param name=\"playerIndex\">The index of the player that should be banned.</param>\n        protected virtual void BanPlayer(int playerIndex)\n        {\n            // Do nothing by default\n        }\n\n        /// <summary>\n        /// Updates the map information labels such as name and author.\n        /// </summary>\n        protected virtual void SetMapLabels()\n        {\n            if (GameMode == null || Map == null)\n            {\n                lblMapName.Text = \"Map: Unknown\".L10N(\"Client:Main:MapUnknown\");\n                lblMapAuthor.Text = \"By Unknown Author\".L10N(\"Client:Main:AuthorByUnknown\");\n                lblGameMode.Text = \"Game mode: Unknown\".L10N(\"Client:Main:GameModeUnknown\");\n                lblMapSize.Text = \"Size: Not available\".L10N(\"Client:Main:MapSizeUnknown\");\n                return;\n            }\n\n            lblMapName.Text = \"Map:\".L10N(\"Client:Main:Map\") + \" \" + Renderer.GetSafeString(Map.Name, lblMapName.FontIndex);\n            lblMapAuthor.Text = \"By\".L10N(\"Client:Main:AuthorBy\") + \" \" + Renderer.GetSafeString(Map.Author, lblMapAuthor.FontIndex);\n            lblGameMode.Text = \"Game mode:\".L10N(\"Client:Main:GameModeLabel\") + \" \" + GameMode.UIName;\n            lblMapSize.Text = \"Size:\".L10N(\"Client:Main:MapSize\") + \" \" + Map.GetSizeString();\n        }\n\n        /// <summary>\n        /// Changes the current map and game mode.\n        /// </summary>\n        /// <param name=\"gameModeMap\">The new game mode map.</param>\n        protected virtual void ChangeMap(GameModeMap gameModeMap)\n        {\n            GameModeMap = gameModeMap;\n\n            _ = UpdateLaunchGameButtonStatus();\n\n            SetMapLabels();\n\n            if (GameMode == null || Map == null)\n            {\n                MapPreviewBox.GameModeMap = null;\n                OnGameOptionChanged();\n                return;\n            }\n\n            disableGameOptionUpdateBroadcast = true;\n\n            // Clear forced options\n            foreach (var ddGameOption in DropDowns)\n                ddGameOption.AllowDropDown = true;\n\n            foreach (var checkBox in CheckBoxes)\n                checkBox.AllowChecking = true;\n\n            // We could either pass the CheckBoxes and DropDowns of this class\n            // to the Map and GameMode instances and let them apply their forced\n            // options, or we could do it in this class with helper functions.\n            // The second approach is probably clearer.\n\n            // We use these temp lists to determine which options WERE NOT forced\n            // by the map. We then return these to user-defined settings.\n            // This prevents forced options from one map getting carried\n            // to other maps.\n\n            var checkBoxListClone = new List<GameLobbyCheckBox>(CheckBoxes);\n            var dropDownListClone = new List<GameLobbyDropDown>(DropDowns);\n\n            ApplyForcedCheckBoxOptions(checkBoxListClone, GameMode.ForcedCheckBoxValues);\n            ApplyForcedCheckBoxOptions(checkBoxListClone, Map.ForcedCheckBoxValues);\n\n            ApplyForcedDropDownOptions(dropDownListClone, GameMode.ForcedDropDownValues);\n            ApplyForcedDropDownOptions(dropDownListClone, Map.ForcedDropDownValues);\n\n            foreach (var chkBox in checkBoxListClone)\n                chkBox.Checked = chkBox.HostChecked;\n\n            foreach (var dd in dropDownListClone)\n                dd.SelectedIndex = dd.HostSelectedIndex;\n\n            // Enable all sides by default\n            foreach (var ddSide in ddPlayerSides)\n            {\n                ddSide.Items.ForEach(item => item.Selectable = true);\n            }\n\n            // Enable all colors by default\n            foreach (var ddColor in ddPlayerColors)\n            {\n                for (int i = 0; i < ddColor.Items.Count; i++)\n                {\n                    ddColor.Items[i].Selectable = true;\n                    ddColor.SetItemColorEnabled(i, true);\n                }\n            }\n\n            // Apply starting locations\n            foreach (var ddStart in ddPlayerStarts)\n            {\n                ddStart.Items.Clear();\n\n                ddStart.AddItem(\"???\");\n\n                int maxLocation = GameModeMap.MaxPlayers == 0 ? 0 : (GameModeMap.AllowedStartingLocations.Max() == GameModeMap.MaxPlayers ? GameModeMap.MaxPlayers : MAX_PLAYER_COUNT);\n                for (int i = 1; i <= maxLocation; i++)\n                {\n                    if (GameModeMap.AllowedStartingLocations.Contains(i))\n                        ddStart.AddItem(i.ToString());\n                    else\n                        ddStart.AddItem(new XNADropDownItem() { Text = i.ToString(), Selectable = false });\n                }\n            }\n\n\n            // Check if AI players allowed\n            bool AIAllowed = !GameModeMap.HumanPlayersOnly;\n            foreach (var ddName in ddPlayerNames)\n            {\n                if (ddName.Items.Count > 3)\n                {\n                    ddName.Items[1].Selectable = AIAllowed;\n                    ddName.Items[2].Selectable = AIAllowed;\n                    ddName.Items[3].Selectable = AIAllowed;\n                }\n            }\n\n            if (!AIAllowed) AIPlayers.Clear();\n            IEnumerable<PlayerInfo> concatPlayerList = Players.Concat(AIPlayers).ToList();\n\n            foreach (PlayerInfo pInfo in concatPlayerList)\n            {\n                if (!GameModeMap.AllowedStartingLocations.Contains(pInfo.StartingLocation) ||\n                    GameModeMap.ForceRandomStartLocations)\n                    pInfo.StartingLocation = 0;\n                if (!GameModeMap.IsCoop && GameModeMap.ForceNoTeams)\n                    pInfo.TeamId = 0;\n            }\n\n\n            if (GameModeMap.CoopInfo != null)\n            {\n                // Co-Op map disallowed color logic\n                foreach (int disallowedColorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors)\n                {\n                    if (disallowedColorIndex >= MPColors.Count)\n                        continue;\n\n                    foreach (var ddColor in ddPlayerColors)\n                    {\n                        ddColor.Items[disallowedColorIndex + 1].Selectable = false;\n                        ddColor.SetItemColorEnabled(disallowedColorIndex + 1, false);\n                    }\n\n                    foreach (PlayerInfo pInfo in concatPlayerList)\n                    {\n                        if (pInfo.ColorId == disallowedColorIndex + 1)\n                            pInfo.ColorId = 0;\n                    }\n                }\n\n                // Force teams\n                foreach (PlayerInfo pInfo in concatPlayerList)\n                    pInfo.TeamId = 1;\n\n                if (PlayerExtraOptionsPanel != null)\n                {\n                    PlayerExtraOptionsPanel.ForcedNoTeamsAllowChecking = false;\n                    PlayerExtraOptionsPanel.ForcedNoTeams = false;\n\n                    PlayerExtraOptionsPanel.UseTeamStartMappingsAllowChecking = false;\n                    PlayerExtraOptionsPanel.UseTeamStartMappings = false;\n                }\n            }\n            else\n            {\n                if (PlayerExtraOptionsPanel != null)\n                {\n                    PlayerExtraOptionsPanel.ForcedNoTeamsAllowChecking = true;\n                    PlayerExtraOptionsPanel.UseTeamStartMappingsAllowChecking = true;\n                }\n            }\n\n            OnGameOptionChanged();\n\n            MapPreviewBox.GameModeMap = GameModeMap;\n            CopyPlayerDataToUI();\n\n            disableGameOptionUpdateBroadcast = false;\n\n            PlayerExtraOptionsPanel?.UpdateForGameModeMap(GameModeMap);\n        }\n\n        private void ApplyForcedCheckBoxOptions(List<GameLobbyCheckBox> optionList,\n            List<KeyValuePair<string, bool>> forcedOptions)\n        {\n            foreach (KeyValuePair<string, bool> option in forcedOptions)\n            {\n                GameLobbyCheckBox checkBox = CheckBoxes.Find(chk => chk.Name == option.Key);\n                if (checkBox != null)\n                {\n                    checkBox.Checked = option.Value;\n                    checkBox.AllowChecking = false;\n                    optionList.Remove(checkBox);\n                }\n            }\n        }\n\n        private void ApplyForcedDropDownOptions(List<GameLobbyDropDown> optionList,\n            List<KeyValuePair<string, int>> forcedOptions)\n        {\n            foreach (KeyValuePair<string, int> option in forcedOptions)\n            {\n                GameLobbyDropDown dropDown = DropDowns.Find(dd => dd.Name == option.Key);\n                if (dropDown != null)\n                {\n                    dropDown.SelectedIndex = option.Value;\n                    dropDown.AllowDropDown = false;\n                    optionList.Remove(dropDown);\n                }\n            }\n        }\n\n        protected string AILevelToName(int aiLevel)\n        {\n            return ProgramConstants.GetAILevelName(aiLevel);\n        }\n\n        protected GameType GetGameType()\n        {\n            int teamCount = GetPvPTeamCount();\n\n            if (teamCount == 0)\n                return GameType.FFA;\n\n            if (teamCount == 1)\n                return GameType.Coop;\n\n            return GameType.TeamGame;\n        }\n\n        protected Rank GetRank()\n        {\n            if (GameMode == null || Map == null)\n                return Rank.None;\n\n            foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n            {\n                if (checkBox.AllowScoring)\n                    return Rank.None;\n            }\n            \n            foreach (GameLobbyDropDown dropDown in DropDowns)\n            {\n                if (dropDown.AllowScoring)\n                    return Rank.None;\n            }\n\n            PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n\n            if (localPlayer == null)\n                return Rank.None;\n\n            if (IsPlayerSpectator(localPlayer))\n                return Rank.None;\n\n            // These variables are used by both the skirmish and multiplayer code paths\n            int[] teamMemberCounts = new int[5];\n            int lowestEnemyAILevel = 2;\n            int highestAllyAILevel = 0;\n\n            foreach (PlayerInfo aiPlayer in AIPlayers)\n            {\n                teamMemberCounts[aiPlayer.TeamId]++;\n\n                if (aiPlayer.TeamId > 0 && aiPlayer.TeamId == localPlayer.TeamId)\n                {\n                    if (aiPlayer.AILevel > highestAllyAILevel)\n                        highestAllyAILevel = aiPlayer.AILevel;\n                }\n                else\n                {\n                    if (aiPlayer.AILevel < lowestEnemyAILevel)\n                        lowestEnemyAILevel = aiPlayer.AILevel;\n                }\n            }\n\n            if (isMultiplayer)\n            {\n                if (Players.Count == 1)\n                    return Rank.None;\n\n                // PvP stars for 2-player and 3-player maps\n                if (GameModeMap.MaxPlayers <= 3)\n                {\n                    List<PlayerInfo> filteredPlayers = Players.Where(p => !IsPlayerSpectator(p)).ToList();\n\n                    if (AIPlayers.Count > 0)\n                        return Rank.None;\n\n                    if (filteredPlayers.Count != GameModeMap.MaxPlayers)\n                        return Rank.None;\n\n                    int localTeamIndex = localPlayer.TeamId;\n                    if (localTeamIndex > 0 && filteredPlayers.Count(p => p.TeamId == localTeamIndex) > 1)\n                        return Rank.None;\n\n                    return Rank.Hard;\n                }\n\n                // Coop stars for maps with 4 or more players\n                // See the code in StatisticsManager.GetRankForCoopMatch for the conditions\n\n                if (Players.Find(p => IsPlayerSpectator(p)) != null)\n                    return Rank.None;\n\n                if (AIPlayers.Count == 0)\n                    return Rank.None;\n\n                if (Players.Find(p => p.TeamId != localPlayer.TeamId) != null)\n                    return Rank.None;\n\n                if (Players.Find(p => p.TeamId == 0) != null)\n                    return Rank.None;\n\n                if (AIPlayers.Find(p => p.TeamId == 0) != null)\n                    return Rank.None;\n\n                teamMemberCounts[localPlayer.TeamId] += Players.Count;\n\n                if (lowestEnemyAILevel < highestAllyAILevel)\n                {\n                    // Check that the player's AI allies aren't stronger\n                    return Rank.None;\n                }\n\n                // Check that all teams have at least as many players\n                // as the human players' team\n                int allyCount = teamMemberCounts[localPlayer.TeamId];\n\n                for (int i = 1; i < 5; i++)\n                {\n                    if (i == localPlayer.TeamId)\n                        continue;\n\n                    if (teamMemberCounts[i] > 0)\n                    {\n                        if (teamMemberCounts[i] < allyCount)\n                            return Rank.None;\n                    }\n                }\n\n                return lowestEnemyAILevel + 1;\n            }\n\n            // *********\n            // Skirmish!\n            // *********\n\n            if (AIPlayers.Count != GameModeMap.MaxPlayers - 1)\n                return Rank.None;\n\n            teamMemberCounts[localPlayer.TeamId]++;\n\n            if (lowestEnemyAILevel < highestAllyAILevel)\n            {\n                // Check that the player's AI allies aren't stronger\n                return Rank.None;\n            }\n\n            if (localPlayer.TeamId > 0)\n            {\n                // Check that all teams have at least as many players\n                // as the local player's team\n                int allyCount = teamMemberCounts[localPlayer.TeamId];\n\n                for (int i = 1; i < 5; i++)\n                {\n                    if (i == localPlayer.TeamId)\n                        continue;\n\n                    if (teamMemberCounts[i] > 0)\n                    {\n                        if (teamMemberCounts[i] < allyCount)\n                            return Rank.None;\n                    }\n                }\n\n                // Check that there is a team other than the players' team that is at least as large\n                bool pass = false;\n                for (int i = 1; i < 5; i++)\n                {\n                    if (i == localPlayer.TeamId)\n                        continue;\n\n                    if (teamMemberCounts[i] >= allyCount)\n                    {\n                        pass = true;\n                        break;\n                    }\n                }\n\n                if (!pass)\n                    return Rank.None;\n            }\n\n            return lowestEnemyAILevel + 1;\n        }\n\n        protected string AddGameOptionPreset(string name)\n        {\n            string error = GameOptionPreset.IsNameValid(name);\n            if (!string.IsNullOrEmpty(error))\n                return error;\n\n            GameOptionPreset preset = new GameOptionPreset(name);\n            foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n            {\n                preset.AddCheckBoxValue(checkBox.Name, checkBox.Checked);\n            }\n\n            foreach (GameLobbyDropDown dropDown in DropDowns)\n            {\n                preset.AddDropDownValue(dropDown.Name, dropDown.SelectedIndex);\n            }\n\n            GameOptionPresets.Instance.AddPreset(preset);\n            return null;\n        }\n\n        public bool LoadGameOptionPreset(string name)\n        {\n            GameOptionPreset preset = GameOptionPresets.Instance.GetPreset(name);\n            if (preset == null)\n                return false;\n\n            disableGameOptionUpdateBroadcast = true;\n\n            var checkBoxValues = preset.GetCheckBoxValues();\n            foreach (var kvp in checkBoxValues)\n            {\n                GameLobbyCheckBox checkBox = CheckBoxes.Find(c => c.Name == kvp.Key);\n                if (checkBox != null && checkBox.AllowChanges && checkBox.AllowChecking)\n                {\n                    checkBox.Checked = kvp.Value;\n                    checkBox.HostChecked = kvp.Value;\n                }\n            }\n\n            var dropDownValues = preset.GetDropDownValues();\n            foreach (var kvp in dropDownValues)\n            {\n                GameLobbyDropDown dropDown = DropDowns.Find(d => d.Name == kvp.Key);\n                if (dropDown != null && dropDown.AllowDropDown)\n                {\n                    dropDown.SelectedIndex = kvp.Value;\n                    dropDown.HostSelectedIndex = kvp.Value;\n                }\n            }\n\n            disableGameOptionUpdateBroadcast = false;\n            OnGameOptionChanged();\n            return true;\n        }\n\n        /// <summary>\n        /// Checks if launch game button can stay enabled or not and updates the state accordingly.\n        /// </summary>\n        /// <returns>True if launch game button is enabled, false if not.</returns>\n        protected virtual bool UpdateLaunchGameButtonStatus()\n        {\n            return true;\n        }\n\n        protected abstract bool AllowPlayerOptionsChange();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyCheckBox.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore.Extensions;\n\nusing DTAClient.DXGUI.Generic;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby;\n\npublic class GameLobbyCheckBox : GameSessionCheckBox\n{\n    public GameLobbyCheckBox(WindowManager windowManager) : base(windowManager) { }\n\n    public bool IsMultiplayer { get; set; }\n\n    /// <summary>\n    /// The last host-defined value for this check box.\n    /// Defaults to the default value of Checked after the check-box\n    /// has been initialized, but its value is only changed by user interaction.\n    /// </summary>\n    public bool HostChecked { get; set; }\n\n    /// <summary>\n    /// The last value that the local player gave for this check box.\n    /// Defaults to the default value of Checked after the check-box\n    /// has been initialized, but its value is only changed by user interaction.\n    /// </summary>\n    public bool UserChecked { get; set; }\n\n    /// <summary>\n    /// The side indices that this check box disallows when checked.\n    /// Defaults to -1, which means none.\n    /// </summary>\n    public List<int> DisallowedSideIndices = new();\n\n    public override void Initialize()\n    {\n        // Find the game lobby that this control belongs to and register ourselves as a game option.\n\n        XNAControl parent = Parent;\n        while (true)\n        {\n            if (parent == null)\n                break;\n\n            // oh no, we have a circular class reference here!\n            if (parent is GameLobbyBase configView)\n            {\n                configView.CheckBoxes.Add(this);\n                break;\n            }\n\n            parent = parent.Parent;\n        }\n\n        base.Initialize();\n    }\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        switch (key)\n        {\n            case \"CheckedMP\":\n                if (IsMultiplayer)\n                    Checked = Conversions.BooleanFromString(value, false);\n                return;\n            case \"Checked\":\n                bool checkedValue = Conversions.BooleanFromString(value, false);\n                HostChecked = checkedValue;\n                UserChecked = checkedValue;\n                break;  // let base method handle it too as we're not replacing it fully\n            case \"DisallowedSideIndex\":\n            case \"DisallowedSideIndices\":\n                List<int> sides = value.SplitWithCleanup()\n                    .Select(s => Conversions.IntFromString(s, -1))\n                    .Distinct()\n                    .ToList();\n                DisallowedSideIndices.AddRange(sides.Where(s => !DisallowedSideIndices.Contains(s)));\n                return;\n        }\n\n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n\n    /// <summary>\n    /// Applies the check-box's disallowed side index to a bool\n    /// array that determines which sides are disabled.\n    /// </summary>\n    /// <param name=\"disallowedArray\">An array that determines which sides are disabled.</param>\n    public void ApplyDisallowedSideIndex(bool[] disallowedArray)\n    {\n        if (DisallowedSideIndices == null || DisallowedSideIndices.Count == 0)\n            return;\n\n        if (Checked != reversed)\n        {\n            for (int i = 0; i < DisallowedSideIndices.Count; i++)\n            {\n                int sideNotAllowed = DisallowedSideIndices[i];\n                disallowedArray[sideNotAllowed] = true;\n            }\n        }\n    }\n\n    public override void OnLeftClick(InputEventArgs inputEventArgs)\n    {\n        // FIXME there's a discrepancy with how base XNAUI handles this\n        // it doesn't set handled if changing the setting is not allowed\n        inputEventArgs.Handled = true;\n\n        if (!AllowChanges)\n            return;\n\n        base.OnLeftClick(inputEventArgs);\n        UserChecked = Checked;\n    }\n\n    public override void Draw(GameTime gameTime)\n    {\n        if (ShowIconInGameLobby)\n        {\n            string iconName = Checked ? EnabledIcon : DisabledIcon;\n            if (!string.IsNullOrEmpty(iconName))\n            {\n                Texture2D icon = AssetLoader.LoadTexture(iconName);\n                if (icon != null)\n                {\n                    const int iconSpacing = 6;\n                    int iconX = -icon.Width - iconSpacing;\n                    int iconY = (Height - icon.Height) / 2;\n\n                    DrawTexture(icon, new Rectangle(iconX, iconY, icon.Width, icon.Height), Color.White);\n                }\n            }\n        }\n\n        base.Draw(gameTime);\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs",
    "content": "using DTAClient.DXGUI.Generic;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby;\n\npublic class GameLobbyDropDown : GameSessionDropDown\n{\n    public GameLobbyDropDown(WindowManager windowManager) : base(windowManager) { }\n\n    public int HostSelectedIndex { get; set; }\n\n    public int UserSelectedIndex { get; set; }\n\n    public override void Initialize()\n    {\n        // Find the game lobby that this control belongs to and register ourselves as a game option.\n\n        XNAControl parent = Parent;\n        while (true)\n        {\n            if (parent == null)\n                break;\n\n            // oh no, we have a circular class reference here!\n            if (parent is GameLobbyBase configView)\n            {\n                configView.DropDowns.Add(this);\n                break;\n            }\n\n            parent = parent.Parent;\n        }\n\n        base.Initialize();\n    }\n\n    protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)\n    {\n        if (key == \"DefaultIndex\")\n        {\n            int index = int.Parse(value);\n            HostSelectedIndex = index;\n            UserSelectedIndex = index;\n            // don't return, let base method handle it's part too\n        }\n\n        base.ParseControlINIAttribute(iniFile, key, value);\n    }\n\n    public override void OnLeftClick(InputEventArgs inputEventArgs)\n    {\n        // FIXME there's a discrepancy with how base XNAUI handles this\n        // it doesn't set handled if changing the setting is not allowed\n        inputEventArgs.Handled = true;\n\n        if (!AllowDropDown)\n            return;\n\n        base.OnLeftClick(inputEventArgs);\n        UserSelectedIndex = SelectedIndex;\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbySettingsEventArgs.cs",
    "content": "using System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby;\n\npublic class GameLobbySettingsEventArgs(string gameRoomName, int maxPlayers, int skillLevel, string password) : EventArgs\n{\n    public string GameRoomName { get; } = gameRoomName;\n    public int MaxPlayers { get; } = maxPlayers;\n    public int SkillLevel { get; } = skillLevel;\n    public string Password { get; } = password;\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbySettingsWindow.cs",
    "content": "using ClientCore;\nusing ClientCore.Extensions;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby;\n\n/// <summary>\n/// A window that allows the host to modify game lobby settings.\n/// </summary>\npublic class GameLobbySettingsWindow(WindowManager windowManager) : XNAWindow(windowManager)\n{\n    public event EventHandler<GameLobbySettingsEventArgs> SettingsChanged;\n\n    private XNATextBox tbGameName;\n    private XNATextBox tbPassword;\n    private XNAClientDropDown ddMaxPlayers;\n    private XNAClientDropDown ddSkillLevel;\n\n    private XNALabel lblRoomName;\n    private XNALabel lblPassword;\n    private XNALabel lblMaxPlayers;\n    private XNALabel lblSkillLevel;\n\n    private XNAClientButton btnSave;\n    private XNAClientButton btnCancel;\n\n    private string[] SkillLevelOptions;\n\n    public override void Initialize()\n    {\n        SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(',');\n\n        Name = \"GameLobbySettingsWindow\";\n        ClientRectangle = new Rectangle(0, 0, 400, 240);\n        BackgroundTexture = AssetLoader.LoadTexture(\"gamecreationoptionsbg.png\");\n\n        lblRoomName = new XNALabel(WindowManager);\n        lblRoomName.Name = nameof(lblRoomName);\n        lblRoomName.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n            UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, UIDesignConstants.EMPTY_SPACE_TOP +\n            UIDesignConstants.CONTROL_VERTICAL_MARGIN, 0, 0);\n        lblRoomName.Text = \"Game room name:\".L10N(\"Client:Main:GameRoomName\");\n\n        tbGameName = new XNATextBox(WindowManager);\n        tbGameName.Name = nameof(tbGameName);\n        tbGameName.MaximumTextLength = 23;\n        tbGameName.ClientRectangle = new Rectangle(Width - 200 - UIDesignConstants.EMPTY_SPACE_SIDES -\n            UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblRoomName.Y - 2, 200, 21);\n\n        int nextY = tbGameName.Bottom + 15;\n\n        lblPassword = new XNALabel(WindowManager);\n        lblPassword.Name = nameof(lblPassword);\n        lblPassword.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0);\n        lblPassword.Text = \"Password:\".L10N(\"Client:Main:LobbyPassword\");\n\n        tbPassword = new XNATextBox(WindowManager);\n        tbPassword.Name = nameof(tbPassword);\n        tbPassword.MaximumTextLength = 20;\n        tbPassword.ClientRectangle = new Rectangle(tbGameName.X, lblPassword.Y - 2, 200, 21);\n\n        nextY = tbPassword.Bottom + 15;\n\n        lblMaxPlayers = new XNALabel(WindowManager);\n        lblMaxPlayers.Name = nameof(lblMaxPlayers);\n        lblMaxPlayers.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0);\n        lblMaxPlayers.Text = \"Max players:\".L10N(\"Client:Main:GameMaxPlayers\");\n\n        ddMaxPlayers = new XNAClientDropDown(WindowManager);\n        ddMaxPlayers.Name = nameof(ddMaxPlayers);\n        ddMaxPlayers.ClientRectangle = new Rectangle(tbGameName.X, lblMaxPlayers.Y - 2,\n            tbGameName.Width, 21);\n        for (int i = 8; i > 1; i--)\n            ddMaxPlayers.AddItem(i.ToString());\n        ddMaxPlayers.SelectedIndex = 0;\n\n        nextY = ddMaxPlayers.Bottom + 15;\n\n        lblSkillLevel = new XNALabel(WindowManager);\n        lblSkillLevel.Name = nameof(lblSkillLevel);\n        lblSkillLevel.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0);\n        lblSkillLevel.Text = \"Preferred skill level:\".L10N(\"Client:Main:PreferredSkillLevel\");\n\n        ddSkillLevel = new XNAClientDropDown(WindowManager);\n        ddSkillLevel.Name = nameof(ddSkillLevel);\n        ddSkillLevel.ClientRectangle = new Rectangle(tbGameName.X, lblSkillLevel.Y - 2,\n            tbGameName.Width, 21);\n\n        for (int i = 0; i < SkillLevelOptions.Length; i++)\n        {\n            string skillLevel = SkillLevelOptions[i];\n            string localizedSkillLevel = skillLevel.L10N($\"INI:ClientDefinitions:SkillLevel:{i}\");\n            ddSkillLevel.AddItem(localizedSkillLevel);\n        }\n\n        ddSkillLevel.SelectedIndex = ClientConfiguration.Instance.DefaultSkillLevelIndex;\n\n        nextY = ddSkillLevel.Bottom + 20;\n\n        btnSave = new XNAClientButton(WindowManager);\n        btnSave.Name = nameof(btnSave);\n        btnSave.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES +\n            UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, nextY, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n        btnSave.Text = \"Save\".L10N(\"Client:Main:ButtonSave\");\n        btnSave.LeftClick += BtnSave_LeftClick;\n\n        btnCancel = new XNAClientButton(WindowManager);\n        btnCancel.Name = nameof(btnCancel);\n        btnCancel.ClientRectangle = new Rectangle(Width - UIDesignConstants.BUTTON_WIDTH_133 - UIDesignConstants.EMPTY_SPACE_SIDES -\n            UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n        btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n        btnCancel.LeftClick += BtnCancel_LeftClick;\n\n        AddChild(lblRoomName);\n        AddChild(tbGameName);\n        AddChild(lblPassword);\n        AddChild(tbPassword);\n        AddChild(lblMaxPlayers);\n        AddChild(ddMaxPlayers);\n        AddChild(lblSkillLevel);\n        AddChild(ddSkillLevel);\n        AddChild(btnSave);\n        AddChild(btnCancel);\n\n        Height = btnSave.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM;\n\n        base.Initialize();\n\n        CenterOnParent();\n    }\n\n    public void Open(string currentGameName, int currentMaxPlayers, int currentSkillLevel, string currentPassword)\n    {\n        tbGameName.Text = currentGameName;\n        tbPassword.Text = currentPassword ?? string.Empty;\n        ddMaxPlayers.SelectedIndex = 8 - currentMaxPlayers;\n        ddSkillLevel.SelectedIndex = currentSkillLevel;\n\n        Enable();\n    }\n\n    private void BtnSave_LeftClick(object sender, EventArgs e)\n    {\n        string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text);\n\n        NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage);\n        if (validationError != NameValidationError.None)\n        {\n            XNAMessageBox.Show(WindowManager, \"Invalid game name\".L10N(\"Client:Main:InvalidGameName\"),\n                errorMessage);\n            return;\n        }\n\n        int maxPlayers = int.Parse(ddMaxPlayers.SelectedItem.Text);\n        int skillLevel = ddSkillLevel.SelectedIndex;\n        string password = tbPassword.Text;\n\n        SettingsChanged?.Invoke(this, new GameLobbySettingsEventArgs(\n            gameName, maxPlayers, skillLevel, password));\n\n        Disable();\n    }\n\n    private void BtnCancel_LeftClick(object sender, EventArgs e)\n    {\n        Disable();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameModeMapFilter.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing DTAClient.Domain.Multiplayer;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class GameModeMapFilter\n    {\n        public Func<List<GameModeMap>> GetGameModeMaps;\n\n        public GameModeMapFilter(Func<List<GameModeMap>> filterAction)\n        {\n            GetGameModeMaps = filterAction;\n        }\n\n        public bool Any() => GetGameModeMaps().Any();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/GameType.cs",
    "content": "﻿namespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public enum GameType\n    {\n        Undefined,\n        FFA,\n        TeamGame,\n        Coop\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs",
    "content": "using ClientCore;\nusing DTAClient.Domain;\nusing DTAClient.Domain.LAN;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.LAN;\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers;\nusing DTAClient.Online;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\n\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class LANGameLobby : MultiplayerGameLobby\n    {\n        private const int GAME_OPTION_SPECIAL_FLAG_COUNT = 5;\n\n        private const double DROPOUT_TIMEOUT = 20.0;\n        private const double GAME_BROADCAST_INTERVAL = 2.0;\n\n        private const string CHAT_COMMAND = \"GLCHAT\";\n        private const string RETURN_COMMAND = \"RETURN\";\n        private const string GET_READY_COMMAND = \"GETREADY\";\n        private const string PLAYER_OPTIONS_REQUEST_COMMAND = \"POREQ\";\n        private const string PLAYER_OPTIONS_BROADCAST_COMMAND = \"POPTS\";\n        private const string PLAYER_JOIN_COMMAND = \"JOIN\";\n        private const string PLAYER_QUIT_COMMAND = \"QUIT\";\n        private const string GAME_OPTIONS_COMMAND = \"OPTS\";\n        private const string PLAYER_READY_REQUEST = \"READY\";\n        private const string LAUNCH_GAME_COMMAND = \"LAUNCH\";\n        private const string FILE_HASH_COMMAND = \"FHASH\";\n        private const string DICE_ROLL_COMMAND = \"DR\";\n        public const string PING = \"PING\";\n\n        public LANGameLobby(WindowManager windowManager, string iniName,\n            TopBar topBar, LANColor[] chatColors, MapLoader mapLoader, DiscordHandler discordHandler, PrivateMessagingWindow pmWindow, Random random) :\n            base(windowManager, iniName, topBar, mapLoader, discordHandler, pmWindow, random)\n        {\n            this.chatColors = chatColors;\n            encoding = Encoding.UTF8;\n            hostCommandHandlers = new CommandHandlerBase[]\n            {\n                new StringCommandHandler(CHAT_COMMAND, GameHost_HandleChatCommand),\n                new NoParamCommandHandler(RETURN_COMMAND, GameHost_HandleReturnCommand),\n                new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, HandlePlayerOptionsRequest),\n                new NoParamCommandHandler(PLAYER_QUIT_COMMAND, HandlePlayerQuit),\n                new StringCommandHandler(PLAYER_READY_REQUEST, GameHost_HandleReadyRequest),\n                new StringCommandHandler(FILE_HASH_COMMAND, HandleFileHashCommand),\n                new StringCommandHandler(DICE_ROLL_COMMAND, Host_HandleDiceRoll),\n                new NoParamCommandHandler(PING, s => { }),\n            };\n\n            playerCommandHandlers = new LANClientCommandHandler[]\n            {\n                new ClientStringCommandHandler(CHAT_COMMAND, Player_HandleChatCommand),\n                new ClientNoParamCommandHandler(GET_READY_COMMAND, HandleGetReadyCommand),\n                new ClientNoParamCommandHandler(PLAYER_QUIT_COMMAND, HandleHostQuit),\n                new ClientStringCommandHandler(RETURN_COMMAND, Player_HandleReturnCommand),\n                new ClientStringCommandHandler(PLAYER_OPTIONS_BROADCAST_COMMAND, HandlePlayerOptionsBroadcast),\n                new ClientStringCommandHandler(PlayerExtraOptions.LAN_MESSAGE_KEY, HandlePlayerExtraOptionsBroadcast),\n                new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, HandleGameLaunchCommand),\n                new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, HandleGameOptionsMessage),\n                new ClientStringCommandHandler(DICE_ROLL_COMMAND, Client_HandleDiceRoll),\n                new ClientNoParamCommandHandler(PING, HandlePing),\n            };\n\n            localGame = ClientConfiguration.Instance.LocalGame;\n\n            WindowManager.GameClosing += WindowManager_GameClosing;\n\n            this.random = random;\n        }\n\n        private void WindowManager_GameClosing(object sender, EventArgs e)\n        {\n            if (client != null && client.Connected)\n                Clear();\n        }\n\n        private void HandleFileHashCommand(string sender, string fileHash)\n        {\n            if (fileHash != localFileHash)\n                AddNotice(string.Format(\"{0} has modified game files! They could be cheating!\".L10N(\"Client:Main:PlayerModifiedFiles\"), sender));\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n            if (pInfo == null)\n                return;\n\n            pInfo.HashReceived = true;\n            CopyPlayerDataToUI();\n        }\n\n        public event EventHandler<LobbyNotificationEventArgs> LobbyNotification;\n        public event EventHandler<GameLeftEventArgs> GameLeft;\n        public event EventHandler<GameBroadcastEventArgs> GameBroadcast;\n\n        private TcpListener listener;\n        private TcpClient client;\n        private volatile bool leaving;\n        private int sessionId;\n\n        private IPEndPoint hostEndPoint;\n        private LANColor[] chatColors;\n        private int chatColorIndex;\n        private Encoding encoding;\n\n        private CommandHandlerBase[] hostCommandHandlers;\n        private LANClientCommandHandler[] playerCommandHandlers;\n\n        private TimeSpan timeSinceGameBroadcast = TimeSpan.Zero;\n\n        private TimeSpan timeSinceLastReceivedCommand = TimeSpan.Zero;\n\n        private string overMessage = string.Empty;\n\n        private string localGame;\n\n        private string localFileHash;\n\n        private Random random;\n\n        public override void Initialize()\n        {\n            IniNameOverride = nameof(LANGameLobby);\n            base.Initialize();\n            PostInitialize();\n        }\n\n        public void SetUp(bool isHost,\n            IPEndPoint hostEndPoint, TcpClient client)\n        {\n            leaving = false;\n            sessionId++;\n            Refresh(isHost);\n\n            this.hostEndPoint = hostEndPoint;\n\n            if (isHost)\n            {\n                RandomSeed = random.Next();\n                Thread thread = new Thread(ListenForClients);\n                thread.Start();\n\n                this.client = new TcpClient();\n                this.client.Connect(\"127.0.0.1\", ProgramConstants.LAN_GAME_LOBBY_PORT);\n\n                byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND +\n                    ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME);\n\n                this.client.GetStream().Write(buffer, 0, buffer.Length);\n                this.client.GetStream().Flush();\n\n                var fhc = new FileHashCalculator();\n                fhc.CalculateHashes();\n                localFileHash = fhc.GetCompleteHash();\n\n                RefreshMapSelectionUI();\n            }\n            else\n            {\n                this.client = client;\n            }\n\n            new Thread(HandleServerCommunication).Start();\n\n            if (IsHost)\n                CopyPlayerDataToUI();\n\n            WindowManager.SelectedControl = tbChatInput;\n        }\n\n        public void PostJoin()\n        {\n            var fhc = new FileHashCalculator();\n            fhc.CalculateHashes();\n            SendMessageToHost(FILE_HASH_COMMAND + \" \" + fhc.GetCompleteHash());\n            ResetAutoReadyCheckbox();\n        }\n\n        #region Server code\n\n        private void ListenForClients()\n        {\n            listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT);\n            listener.Start();\n\n            while (true)\n            {\n                TcpClient client;\n\n                try\n                {\n                    client = listener.AcceptTcpClient();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Listener error: \" + ex.ToString());\n                    break;\n                }\n\n                Logger.Log(\"New client connected from \" + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString());\n\n                if (Players.Count >= MAX_PLAYER_COUNT)\n                {\n                    Logger.Log(\"Dropping client because of player limit.\");\n                    client.Close();\n                    continue;\n                }\n\n                if (Locked)\n                {\n                    Logger.Log(\"Dropping client because the game room is locked.\");\n                    client.Close();\n                    continue;\n                }\n\n                LANPlayerInfo lpInfo = new LANPlayerInfo(encoding);\n                lpInfo.SetClient(client);\n\n                Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection));\n                thread.Start(lpInfo);\n            }\n        }\n\n        private void HandleClientConnection(object clientInfo)\n        {\n            var lpInfo = (LANPlayerInfo)clientInfo;\n\n            byte[] message = new byte[1024];\n\n            while (true)\n            {\n                int bytesRead = 0;\n\n                try\n                {\n                    bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Socket error with client \" + lpInfo.IPAddress + \"; removing. Message: \" + ex.ToString());\n                    break;\n                }\n\n                if (bytesRead == 0)\n                {\n                    Logger.Log(\"Connect attempt from \" + lpInfo.IPAddress + \" failed! (0 bytes read)\");\n\n                    break;\n                }\n\n                string msg = encoding.GetString(message, 0, bytesRead);\n\n                string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR);\n                string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n                if (parts.Length != 2)\n                    break;\n\n                string name = parts[1].Trim();\n\n                if (parts[0] == \"JOIN\" && !string.IsNullOrEmpty(name))\n                {\n                    lpInfo.Name = name;\n\n                    AddCallback(new Action<LANPlayerInfo>(AddPlayer), lpInfo);\n                    return;\n                }\n\n                break;\n            }\n\n            if (lpInfo.TcpClient.Connected)\n                lpInfo.TcpClient.Close();\n        }\n\n        private void AddPlayer(LANPlayerInfo lpInfo)\n        {\n            if (Players.Find(p => p.Name == lpInfo.Name) != null ||\n                Players.Count >= MAX_PLAYER_COUNT || Locked)\n                return;\n\n            Players.Add(lpInfo);\n\n            if (IsHost && Players.Count == 1)\n                Players[0].Ready = true;\n\n            lpInfo.MessageReceived += LpInfo_MessageReceived;\n            lpInfo.ConnectionLost += LpInfo_ConnectionLost;\n\n            AddNotice(string.Format(\"{0} connected from {1}\".L10N(\"Client:Main:PlayerFromIP\"), lpInfo.Name, lpInfo.IPAddress));\n            lpInfo.StartReceiveLoop();\n\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n            BroadcastPlayerExtraOptions();\n            OnGameOptionChanged();\n            UpdateDiscordPresence();\n        }\n\n        private void LpInfo_ConnectionLost(object sender, EventArgs e)\n        {\n            AddCallback(new Action<LANPlayerInfo>(HandleConnectionLost), (LANPlayerInfo)sender);\n        }\n\n        private void HandleConnectionLost(LANPlayerInfo lpInfo)\n        {\n            CleanUpPlayer(lpInfo);\n            Players.Remove(lpInfo);\n\n            AddNotice(string.Format(\"{0} has left the game.\".L10N(\"Client:Main:PlayerLeftGame\"), lpInfo.Name));\n\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n\n            if (lpInfo.Name == ProgramConstants.PLAYERNAME)\n                ResetDiscordPresence();\n            else\n                UpdateDiscordPresence();\n        }\n\n        private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e)\n        {\n            AddCallback(new Action<string, LANPlayerInfo>(HandleClientMessage),\n                e.Message, (LANPlayerInfo)sender);\n        }\n\n        private void HandleClientMessage(string data, LANPlayerInfo lpInfo)\n        {\n            lpInfo.TimeSinceLastReceivedMessage = TimeSpan.Zero;\n\n            foreach (CommandHandlerBase cmdHandler in hostCommandHandlers)\n            {\n                if (cmdHandler.Handle(lpInfo.Name, data))\n                    return;\n            }\n\n            Logger.Log(\"Unknown LAN command from \" + lpInfo.ToString() + \" : \" + data);\n        }\n\n        private void CleanUpPlayer(LANPlayerInfo lpInfo)\n        {\n            lpInfo.MessageReceived -= LpInfo_MessageReceived;\n            lpInfo.ConnectionLost -= LpInfo_ConnectionLost;\n            lpInfo.TcpClient.Close();\n        }\n\n        #endregion\n\n        private void HandleServerCommunication()\n        {\n            byte[] message = new byte[1024];\n\n            var msg = string.Empty;\n\n            int bytesRead = 0;\n\n            int mySessionId = sessionId;\n\n            if (!client.Connected)\n                return;\n\n            var stream = client.GetStream();\n\n            while (true)\n            {\n                bytesRead = 0;\n\n                try\n                {\n                    bytesRead = stream.Read(message, 0, message.Length);\n                }\n                catch (Exception ex)\n                {\n                    // Disconnect from server\n\n                    if (leaving)\n                        break;\n\n                    Logger.Log(string.Format(\n                        \"Reading data from the server failed! Server address: {0}. Exception: {1}\",\n                        hostEndPoint.Address.ToString(), ex.ToString()));\n\n                    string localizedMessage = string.Format(\n                        \"Reading data from the server failed! Server address: {0}. Exception: {1}\".L10N(\"Client:Main:LanServerReadError\"),\n                         hostEndPoint.Address.ToString(), ex.Message);\n\n                    AddCallback(() =>\n                    {\n                        if (sessionId == mySessionId)\n                            LeaveGame(localizedMessage);\n                    });\n                    break;\n                }\n\n                if (bytesRead > 0)\n                {\n                    msg = encoding.GetString(message, 0, bytesRead);\n\n                    msg = overMessage + msg;\n                    List<string> commands = new List<string>();\n\n                    while (true)\n                    {\n                        int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n                        if (index == -1)\n                        {\n                            overMessage = msg;\n                            break;\n                        }\n                        else\n                        {\n                            commands.Add(msg.Substring(0, index));\n                            msg = msg.Substring(index + 1);\n                        }\n                    }\n\n                    foreach (string cmd in commands)\n                    {\n                        string capturedCmd = cmd;\n                        AddCallback(() =>\n                        {\n                            if (sessionId == mySessionId)\n                                HandleMessageFromServer(capturedCmd);\n                        });\n                    }\n\n                    continue;\n                }\n\n                // Disconnect from server\n                if (leaving)\n                    break;\n\n                {\n                    Logger.Log(string.Format(\n                        \"Reading data from the server failed (0 bytes received)! Server address: {0}\", hostEndPoint.Address.ToString()));\n\n                    string localizedMessage = string.Format(\n                        \"Reading data from the server failed (0 bytes received)! Server address: {0}\".L10N(\"Client:Main:LanServerReadZero\"),\n                         hostEndPoint.Address.ToString());\n\n                    AddCallback(() =>\n                    {\n                        if (sessionId == mySessionId)\n                            LeaveGame(localizedMessage);\n                    });\n                }\n\n                break;\n            }\n        }\n\n        private void HandleMessageFromServer(string message)\n        {\n            timeSinceLastReceivedCommand = TimeSpan.Zero;\n\n            foreach (var cmdHandler in playerCommandHandlers)\n            {\n                if (cmdHandler.Handle(message))\n                    return;\n            }\n\n            Logger.Log(\"Unknown LAN command from the server: \" + message);\n        }\n\n        protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGame();\n\n        protected void LeaveGame(string message = null)\n        {\n            if (leaving)\n                return;\n\n            Clear();\n            GameLeft?.Invoke(this, new GameLeftEventArgs() { Message = message });\n            PlayerExtraOptionsPanel?.Disable();\n            Disable();\n        }\n\n        protected override void UpdateDiscordPresence(bool resetTimer = false)\n        {\n            if (discordHandler == null)\n                return;\n\n            PlayerInfo player = FindLocalPlayer();\n            if (player == null || Map == null || GameMode == null)\n                return;\n            string side = \"\";\n            if (ddPlayerSides.Length > Players.IndexOf(player))\n                side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag;\n            string currentState = ProgramConstants.IsInGame ? \"In Game\" : \"In Lobby\"; // not UI strings\n\n            discordHandler.UpdatePresence(\n                Map.UntranslatedName, GameMode.UntranslatedUIName, \"LAN\",\n                currentState, Players.Count, 8, side,\n                \"LAN Game\", IsHost, false, Locked, resetTimer);\n        }\n\n        public override void Clear()\n        {\n            if (IsHost)\n            {\n                GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(\"GAMECLOSED\"));\n                BroadcastMessage(PLAYER_QUIT_COMMAND);\n                Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p));\n                listener.Stop();\n            }\n            else\n            {\n                SendMessageToHost(PLAYER_QUIT_COMMAND);\n            }\n\n            base.Clear();\n\n            leaving = true;\n\n            if (this.client.Connected)\n                this.client.Close();\n\n            ResetDiscordPresence();\n        }\n\n        public void SetChatColorIndex(int colorIndex)\n        {\n            chatColorIndex = colorIndex;\n            tbChatInput.TextColor = chatColors[colorIndex].XNAColor;\n        }\n\n        public override string GetSwitchName() => \"LAN Game Lobby\".L10N(\"Client:Main:LANGameLobby\");\n\n        protected override void AddNotice(string message, Color color) =>\n            lbChatMessages.AddMessage(null, message, color);\n\n        protected override void BroadcastPlayerOptions()\n        {\n            if (!IsHost)\n                return;\n\n            var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_BROADCAST_COMMAND + \" \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            foreach (PlayerInfo pInfo in Players.Concat(AIPlayers))\n            {\n                sb.Append(pInfo.Name);\n                sb.Append(pInfo.SideId);\n                sb.Append(pInfo.ColorId);\n                sb.Append(pInfo.StartingLocation);\n                sb.Append(pInfo.TeamId);\n                if (pInfo.AutoReady && !pInfo.IsInGame && !LastMapChangeWasInvalid)\n                    sb.Append(2);\n                else\n                    sb.Append(Convert.ToInt32(pInfo.IsAI || pInfo.Ready));\n                sb.Append(pInfo.IPAddress);\n                if (pInfo.IsAI)\n                    sb.Append(pInfo.AILevel);\n                else\n                    sb.Append(\"-1\");\n            }\n\n            BroadcastMessage(sb.ToString());\n        }\n\n        protected override void BroadcastPlayerExtraOptions()\n        {\n            var playerExtraOptions = GetPlayerExtraOptions();\n\n            BroadcastMessage(playerExtraOptions.ToLanMessage(), true);\n        }\n\n        protected override void HostLaunchGame() => BroadcastMessage(LAUNCH_GAME_COMMAND + \" \" + UniqueGameID);\n\n        protected override string GetIPAddressForPlayer(PlayerInfo player)\n        {\n            var lpInfo = (LANPlayerInfo)player;\n            return lpInfo.IPAddress;\n        }\n\n        protected override void RequestPlayerOptions(int side, int color, int start, int team)\n        {\n            var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_REQUEST_COMMAND + \" \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            sb.Append(side);\n            sb.Append(color);\n            sb.Append(start);\n            sb.Append(team);\n            SendMessageToHost(sb.ToString());\n        }\n\n        protected override void RequestReadyStatus() =>\n            SendMessageToHost(PLAYER_READY_REQUEST + \" \" + Convert.ToInt32(chkAutoReady.Checked));\n\n        protected override void SendChatMessage(string message)\n        {\n            var sb = new ExtendedStringBuilder(CHAT_COMMAND + \" \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            sb.Append(chatColorIndex);\n            sb.Append(message);\n            SendMessageToHost(sb.ToString());\n        }\n\n        protected override void OnGameOptionChanged()\n        {\n            base.OnGameOptionChanged();\n\n            if (!IsHost)\n                return;\n\n            var sb = new ExtendedStringBuilder(GAME_OPTIONS_COMMAND + \" \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            foreach (GameLobbyCheckBox chkBox in CheckBoxes)\n            {\n                sb.Append(Convert.ToInt32(chkBox.Checked));\n            }\n\n            foreach (GameLobbyDropDown dd in DropDowns)\n            {\n                sb.Append(dd.SelectedIndex);\n            }\n\n            sb.Append(RandomSeed);\n            sb.Append(Map?.SHA1 ?? string.Empty);\n            sb.Append(GameMode?.Name ?? string.Empty);\n            sb.Append(FrameSendRate);\n            sb.Append(Convert.ToInt32(RemoveStartingLocations));\n\n            BroadcastMessage(sb.ToString());\n        }\n\n        protected override void GetReadyNotification()\n        {\n            base.GetReadyNotification();\n#if WINFORMS\n            WindowManager.FlashWindow();\n#endif\n\n            if (IsHost)\n                BroadcastMessage(GET_READY_COMMAND);\n        }\n\n        protected override void ClearPingIndicators()\n        {\n            // TODO Implement pings for LAN lobbies\n        }\n\n        protected override void UpdatePlayerPingIndicator(PlayerInfo pInfo)\n        {\n            // TODO Implement pings for LAN lobbies\n        }\n\n        /// <summary>\n        /// Broadcasts a command to all players in the game as the game host.\n        /// </summary>\n        /// <param name=\"message\">The command to send.</param>\n        /// <param name=\"otherPlayersOnly\">If true, only send this to other players. Otherwise, even the sender will receive their message.</param>\n        private void BroadcastMessage(string message, bool otherPlayersOnly = false)\n        {\n            if (!IsHost)\n                return;\n\n            foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME))\n            {\n                var lpInfo = (LANPlayerInfo)pInfo;\n                lpInfo.SendMessage(message);\n            }\n        }\n\n        protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e)\n        {\n            base.PlayerExtraOptions_OptionsChanged(sender, e);\n            BroadcastPlayerExtraOptions();\n        }\n\n        private void SendMessageToHost(string message)\n        {\n            if (!client.Connected)\n                return;\n\n            byte[] buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n            NetworkStream ns = client.GetStream();\n\n            try\n            {\n                ns.Write(buffer, 0, buffer.Length);\n                ns.Flush();\n            }\n            catch\n            {\n                Logger.Log(\"Sending message to game host failed!\");\n            }\n        }\n\n        protected override void UnlockGame(bool manual)\n        {\n            Locked = false;\n\n            btnLockGame.Text = \"Lock Game\".L10N(\"Client:Main:LockGame\");\n\n            if (manual)\n                AddNotice(\"You've unlocked the game room.\".L10N(\"Client:Main:RoomUnlockedByYou\"));\n        }\n\n        protected override void LockGame()\n        {\n            Locked = true;\n\n            btnLockGame.Text = \"Unlock Game\".L10N(\"Client:Main:UnlockGame\");\n\n            if (Locked)\n                AddNotice(\"You've locked the game room.\".L10N(\"Client:Main:RoomLockedByYou\"));\n        }\n\n        protected override void GameProcessExited()\n        {\n            base.GameProcessExited();\n\n            SendMessageToHost(RETURN_COMMAND);\n\n            if (IsHost)\n            {\n                RandomSeed = random.Next();\n                OnGameOptionChanged();\n                ClearReadyStatuses();\n                CopyPlayerDataToUI();\n                BroadcastPlayerOptions();\n                BroadcastPlayerExtraOptions();\n\n                if (Players.Count < MAX_PLAYER_COUNT)\n                {\n                    UnlockGame(true);\n                }\n            }\n        }\n\n        private void ReturnNotification(string sender)\n        {\n            AddNotice(string.Format(\"{0} has returned from the game.\".L10N(\"Client:Main:PlayerReturned\"), sender));\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo != null)\n                pInfo.IsInGame = false;\n\n            sndReturnSound.Play();\n            CopyPlayerDataToUI();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (IsHost)\n            {\n                for (int i = 1; i < Players.Count; i++)\n                {\n                    LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i];\n                    if (!lpInfo.Update(gameTime))\n                    {\n                        CleanUpPlayer(lpInfo);\n                        Players.RemoveAt(i);\n                        AddNotice(string.Format(\"{0} - connection timed out\".L10N(\"Client:Main:PlayerTimeout\"), lpInfo.Name));\n                        CopyPlayerDataToUI();\n                        BroadcastPlayerOptions();\n                        BroadcastPlayerExtraOptions();\n                        UpdateDiscordPresence();\n                        i--;\n                    }\n                }\n\n                timeSinceGameBroadcast += gameTime.ElapsedGameTime;\n\n                if (timeSinceGameBroadcast > TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL))\n                {\n                    BroadcastGame();\n                    timeSinceGameBroadcast = TimeSpan.Zero;\n                }\n            }\n            else\n            {\n                timeSinceLastReceivedCommand += gameTime.ElapsedGameTime;\n\n                if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT))\n                {\n                    string localizedMessage = string.Format(\n                        \"Connection to the game host timed out. Server address: {0}\".L10N(\"Client:Main:HostConnectTimeOutWithAddress\"),\n                        hostEndPoint.Address.ToString());\n\n                    LobbyNotification?.Invoke(this,\n                        new LobbyNotificationEventArgs(localizedMessage));\n                    LeaveGame(localizedMessage);\n                }\n            }\n\n            base.Update(gameTime);\n        }\n\n        private void BroadcastGame()\n        {\n            var sb = new ExtendedStringBuilder(\"GAME \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION);\n            sb.Append(ProgramConstants.GAME_VERSION);\n            sb.Append(localGame);\n            sb.Append(Map?.UntranslatedName ?? string.Empty);\n            sb.Append(GameMode?.UntranslatedUIName ?? string.Empty);\n            sb.Append(0); // LoadedGameID\n            var sbPlayers = new StringBuilder();\n            Players.ForEach(p => sbPlayers.Append(p.Name + \",\"));\n            sbPlayers.Remove(sbPlayers.Length - 1, 1);\n            sb.Append(sbPlayers.ToString());\n            sb.Append(Convert.ToInt32(Locked));\n            sb.Append(0); // IsLoadedGame\n            sb.Append(Map?.SHA1);\n\n            GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString()));\n        }\n\n        #region Command Handlers\n\n        private void GameHost_HandleChatCommand(string sender, string data)\n        {\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length < 2)\n                return;\n\n            int colorIndex = Conversions.IntFromString(parts[0], -1);\n\n            if (colorIndex < 0 || colorIndex >= chatColors.Length)\n                return;\n\n            BroadcastMessage(CHAT_COMMAND + \" \" + sender + ProgramConstants.LAN_DATA_SEPARATOR + data);\n        }\n\n        private void Player_HandleChatCommand(string data)\n        {\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length < 3)\n                return;\n\n            string playerName = parts[0];\n\n            int colorIndex = Conversions.IntFromString(parts[1], -1);\n\n            if (colorIndex < 0 || colorIndex >= chatColors.Length)\n                return;\n\n            lbChatMessages.AddMessage(new ChatMessage(playerName,\n                chatColors[colorIndex].XNAColor, DateTime.Now, parts[2]));\n        }\n\n        private void GameHost_HandleReturnCommand(string sender)\n        {\n            BroadcastMessage(RETURN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + sender);\n        }\n\n        private void Player_HandleReturnCommand(string sender)\n        {\n            ReturnNotification(sender);\n        }\n\n        private void HandleGetReadyCommand()\n        {\n            if (!IsHost)\n                GetReadyNotification();\n        }\n\n        private void HandleHostQuit()\n        {\n            if (!IsHost && !leaving)\n                LeaveGame();\n        }\n\n        private void HandlePlayerOptionsRequest(string sender, string data)\n        {\n            if (!IsHost)\n                return;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo == null)\n                return;\n\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length != 4)\n                return;\n\n            int side = Conversions.IntFromString(parts[0], -1);\n            int color = Conversions.IntFromString(parts[1], -1);\n            int start = Conversions.IntFromString(parts[2], -1);\n            int team = Conversions.IntFromString(parts[3], -1);\n\n            if (side < 0 || side > SideCount + RandomSelectorCount)\n                return;\n\n            if (color < 0 || color > MPColors.Count)\n                return;\n\n            var disallowedSides = GetDisallowedSides();\n\n            if (side > 0 && side <= SideCount && disallowedSides[side - 1])\n                return;\n\n            if (GameModeMap.CoopInfo != null)\n            {\n                if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount)\n                    return;\n\n                if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1))\n                    return;\n            }\n\n            if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true)))\n                return;\n\n            if (team < 0 || team > 4)\n                return;\n\n            if (side != pInfo.SideId\n                || start != pInfo.StartingLocation\n                || team != pInfo.TeamId)\n            {\n                ClearReadyStatuses();\n            }\n\n            pInfo.SideId = side;\n            pInfo.ColorId = color;\n            pInfo.StartingLocation = start;\n            pInfo.TeamId = team;\n\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n        }\n\n        private void HandlePlayerExtraOptionsBroadcast(string data) => ApplyPlayerExtraOptions(null, data);\n\n        private void HandlePlayerOptionsBroadcast(string data)\n        {\n            if (IsHost)\n                return;\n\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            int playerCount = parts.Length / 8;\n\n            if (parts.Length != playerCount * 8)\n                return;\n\n            PlayerInfo localPlayer = FindLocalPlayer();\n            int oldSideId = localPlayer == null ? -1 : localPlayer.SideId;\n\n            Players.Clear();\n            AIPlayers.Clear();\n\n            for (int i = 0; i < playerCount; i++)\n            {\n                int baseIndex = i * 8;\n\n                string name = parts[baseIndex];\n                int side = Conversions.IntFromString(parts[baseIndex + 1], -1);\n                int color = Conversions.IntFromString(parts[baseIndex + 2], -1);\n                int start = Conversions.IntFromString(parts[baseIndex + 3], -1);\n                int team = Conversions.IntFromString(parts[baseIndex + 4], -1);\n                int readyStatus = Conversions.IntFromString(parts[baseIndex + 5], -1);\n                string ipAddress = parts[baseIndex + 6];\n                int aiLevel = Conversions.IntFromString(parts[baseIndex + 7], -1);\n\n                if (side < 0 || side > SideCount + RandomSelectorCount)\n                    return;\n\n                if (color < 0 || color > MPColors.Count)\n                    return;\n\n                if (start < 0 || start > MAX_PLAYER_COUNT)\n                    return;\n\n                if (team < 0 || team > 4)\n                    return;\n\n                if (ipAddress == \"127.0.0.1\")\n                    ipAddress = hostEndPoint.Address.ToString();\n\n                bool isAi = aiLevel > -1;\n                if (aiLevel > 2)\n                    return;\n\n                PlayerInfo pInfo;\n\n                if (!isAi)\n                {\n                    pInfo = new LANPlayerInfo(encoding);\n                    pInfo.Name = name;\n                    Players.Add(pInfo);\n                }\n                else\n                {\n                    pInfo = new PlayerInfo();\n                    pInfo.Name = AILevelToName(aiLevel);\n                    pInfo.IsAI = true;\n                    pInfo.AILevel = aiLevel;\n                    AIPlayers.Add(pInfo);\n                }\n\n                pInfo.SideId = side;\n                pInfo.ColorId = color;\n                pInfo.StartingLocation = start;\n                pInfo.TeamId = team;\n                pInfo.Ready = readyStatus > 0;\n                pInfo.AutoReady = readyStatus > 1;\n                pInfo.IPAddress = ipAddress;\n            }\n\n            CopyPlayerDataToUI();\n\n            localPlayer = FindLocalPlayer();\n            if (localPlayer != null && oldSideId != localPlayer.SideId)\n                UpdateDiscordPresence();\n        }\n\n        private void HandlePlayerQuit(string sender)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo == null)\n                return;\n\n            AddNotice(string.Format(\"{0} has left the game.\".L10N(\"Client:Main:PlayerLeftGame\"), pInfo.Name));\n            Players.Remove(pInfo);\n            ClearReadyStatuses();\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n            UpdateDiscordPresence();\n        }\n\n        private void HandleGameOptionsMessage(string data)\n        {\n            if (IsHost)\n                return;\n\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT)\n            {\n                AddNotice((\"The game host has sent an invalid game options message! \" +\n                    \"The game host's game version might be different from yours.\").L10N(\"Client:Main:HostGameOptionInvalid\"));\n                Logger.Log(\"Invalid game options message from host: \" + data);\n                return;\n            }\n\n            int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1);\n            if (randomSeed == -1)\n                return;\n\n            RandomSeed = randomSeed;\n\n            string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)];\n            string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)];\n\n            GameModeMap gameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1);\n\n            if (gameModeMap == null)\n            {\n                ChangeMap(null);\n                if (!string.IsNullOrEmpty(mapSHA1))\n                    AddNotice(\"The game host has selected a map that doesn't exist on your installation.\".L10N(\"Client:Main:MapNotExist\") + \" \" +\n                        \"The host needs to change the map or you won't be able to play.\".L10N(\"Client:Main:HostNeedChangeMapForYou\"));\n\n                return;\n            }\n\n            if (GameModeMap != gameModeMap)\n                ChangeMap(gameModeMap);\n\n            int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate);\n            if (frameSendRate != FrameSendRate)\n            {\n                FrameSendRate = frameSendRate;\n                AddNotice(string.Format(\"The game host has changed FrameSendRate (order lag) to {0}\".L10N(\"Client:Main:HostChangeFrameSendRate\"), frameSendRate));\n            }\n\n            bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(\n                parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations)));\n            SetRandomStartingLocations(removeStartingLocations);\n\n            for (int i = 0; i < CheckBoxes.Count; i++)\n            {\n                GameLobbyCheckBox chkBox = CheckBoxes[i];\n\n                bool oldValue = chkBox.Checked;\n                chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0;\n\n                if (chkBox.Checked != oldValue)\n                {\n                    if (chkBox.Checked)\n                        AddNotice(string.Format(\"The game host has enabled {0}\".L10N(\"Client:Main:HostEnableOption\"), chkBox.Text));\n                    else\n                        AddNotice(string.Format(\"The game host has disabled {0}\".L10N(\"Client:Main:HostDisableOption\"), chkBox.Text));\n                }\n            }\n\n            for (int i = 0; i < DropDowns.Count; i++)\n            {\n                int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1);\n\n                GameLobbyDropDown dd = DropDowns[i];\n\n                if (index < 0 || index >= dd.Items.Count)\n                    return;\n\n                int oldValue = dd.SelectedIndex;\n                dd.SelectedIndex = index;\n\n                if (index != oldValue)\n                {\n                    string ddName = dd.OptionName;\n                    if (dd.OptionName == null)\n                        ddName = dd.Name;\n\n                    AddNotice(string.Format(\"The game host has set {0} to {1}\".L10N(\"Client:Main:HostSetOption\"), ddName, dd.SelectedItem.Text));\n                }\n            }\n        }\n\n        private void GameHost_HandleReadyRequest(string sender, string autoReady)\n        {\n            PlayerInfo pInfo = Players.Find(p => p.Name == sender);\n\n            if (pInfo == null)\n                return;\n\n            pInfo.Ready = true;\n            pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0));\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n        }\n\n        private void HandleGameLaunchCommand(string gameId)\n        {\n            Players.ForEach(pInfo => pInfo.IsInGame = true);\n            UniqueGameID = Conversions.IntFromString(gameId, -1);\n            if (UniqueGameID < 0)\n                return;\n\n            CopyPlayerDataToUI();\n            StartGame();\n        }\n\n        private void HandlePing()\n        {\n            SendMessageToHost(PING);\n        }\n\n        protected override void BroadcastDiceRoll(int dieSides, int[] results)\n        {\n            string resultString = string.Join(\",\", results);\n            SendMessageToHost($\"DR {dieSides},{resultString}\");\n        }\n\n        private void Host_HandleDiceRoll(string sender, string result)\n        {\n            BroadcastMessage($\"{DICE_ROLL_COMMAND} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}\");\n        }\n\n        private void Client_HandleDiceRoll(string data)\n        {\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n            if (parts.Length != 2)\n                return;\n\n            HandleDiceRollResult(parts[0], parts[1]);\n        }\n\n        #endregion\n\n        protected override void WriteSpawnIniAdditions(IniFile iniFile)\n        {\n            base.WriteSpawnIniAdditions(iniFile);\n\n            iniFile.SetIntValue(\"Settings\", \"Port\", ProgramConstants.LAN_INGAME_PORT);\n            iniFile.SetIntValue(\"Settings\", \"GameID\", UniqueGameID);\n            iniFile.SetBooleanValue(\"Settings\", \"Host\", IsHost);\n        }\n    }\n\n    public class LobbyNotificationEventArgs : EventArgs\n    {\n        public LobbyNotificationEventArgs(string notification)\n        {\n            Notification = notification;\n        }\n\n        public string Notification { get; private set; }\n    }\n\n    public class GameBroadcastEventArgs : EventArgs\n    {\n        public GameBroadcastEventArgs(string message)\n        {\n            Message = message;\n        }\n\n        public string Message { get; private set; }\n    }\n\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nusing ClientCore;\nusing ClientCore.I18N;\nusing ClientCore.Extensions;\n\nusing DTAClient.Domain.Multiplayer;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public static class MapCodeHelper\n    {\n        public static Encoding GetMapEncoding(string filepath) => Translation.Instance.MapEncoding ?? FileExtensions.GetDetectedEncoding(filepath);\n\n        /// <summary>\n        /// Applies code from a component custom INI file to a map INI file.\n        /// </summary>\n        /// <param name=\"mapIni\">The map INI file.</param>\n        /// <param name=\"customIniPath\">The custom INI file path.</param>\n        /// <param name=\"gameMode\">Currently selected gamemode, if set.</param>\n        public static void ApplyMapCode(IniFile mapIni, string customIniPath, GameMode gameMode)\n        {\n            string associatedIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, customIniPath);\n            Encoding associatedIniEncoding = GetMapEncoding(associatedIniPath);\n            IniFile associatedIni = new IniFile(associatedIniPath, associatedIniEncoding);\n            string extraIniName = null;\n            if (gameMode != null)\n                extraIniName = associatedIni.GetStringValue(\"GameModeIncludes\", gameMode.Name, null);\n            associatedIni.EraseSectionKeys(\"GameModeIncludes\");\n            ApplyMapCode(mapIni, associatedIni);\n            if (!String.IsNullOrEmpty(extraIniName))\n            {\n                string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, extraIniName);\n                Encoding extraIniEncoding = GetMapEncoding(extraIniPath);\n                ApplyMapCode(mapIni, new IniFile(extraIniPath, extraIniEncoding));\n            }\n        }\n\n        /// <summary>\n        /// Apply map code from an arbitrary INI file to a map INI file.\n        /// </summary>\n        /// <param name=\"mapIni\">The map INI file.</param>\n        /// <param name=\"mapCodeIni\">The INI file to apply to map INI file.</param>\n        public static void ApplyMapCode(IniFile mapIni, IniFile mapCodeIni)\n        {\n            ReplaceMapObjects(mapIni, mapCodeIni, \"Aircraft\");\n            ReplaceMapObjects(mapIni, mapCodeIni, \"Infantry\");\n            ReplaceMapObjects(mapIni, mapCodeIni, \"Units\");\n            ReplaceMapObjects(mapIni, mapCodeIni, \"Structures\");\n            ReplaceMapObjects(mapIni, mapCodeIni, \"Terrain\");\n            IniFile.ConsolidateIniFiles(mapIni, mapCodeIni);\n        }\n\n        /// <summary>\n        /// Replace all instances of objects defined in specific map section that match ID's with new object ID's.\n        /// </summary>\n        /// <param name=\"mapIni\">The map INI file.</param>\n        /// <param name=\"mapCodeIni\">The INI file to apply to map INI file.</param>\n        /// <param name=\"sectionName\">The object section ID.</param>\n        private static void ReplaceMapObjects(IniFile mapIni, IniFile mapCodeIni, string sectionName)\n        {\n            string replaceSectionName = \"ReplaceMap\" + sectionName;\n\n            List<KeyValuePair<string, string>> objectRemapPairs = GetKeyValuePairs(mapCodeIni, replaceSectionName);\n            if (objectRemapPairs.Count < 1) return;\n\n            List<KeyValuePair<string, string>> sectionKeyValuePairs = GetKeyValuePairs(mapIni, sectionName);\n\n            foreach (KeyValuePair<string, string> objectRemapPair in objectRemapPairs)\n            {\n                List<KeyValuePair<string, string>> matchingSectionKVPs =\n                    sectionKeyValuePairs.Where(x => GetObjectID(x.Value, sectionName) == objectRemapPair.Key).ToList();\n\n                foreach (KeyValuePair<string, string> matchingSectionKVP in matchingSectionKVPs)\n                {\n                    string id = GetObjectID(matchingSectionKVP.Value, sectionName);\n\n                    if (!String.IsNullOrEmpty(objectRemapPair.Value))\n                    {\n                        mapIni.SetStringValue(sectionName, matchingSectionKVP.Key, matchingSectionKVP.Value.Replace(id, objectRemapPair.Value));\n                        Logger.Log(\"MapCodeHelper: Changed an instance of '\" + sectionName + \"' object '\" + id + \"' into '\" + objectRemapPair.Value + \"'.\");\n                    }\n                    else\n                    {\n                        mapIni.SetStringValue(sectionName, matchingSectionKVP.Key, \"\");\n                        Logger.Log(\"MapCodeHelper: Removed an instance of '\" + sectionName + \"' object '\" + id + \"'.\");\n                    }\n                }\n            }\n\n            mapCodeIni.EraseSectionKeys(replaceSectionName);\n        }\n\n        /// <summary>\n        /// Get object ID from an object section value.\n        /// </summary>\n        /// <param name=\"value\">Object section value.</param>\n        /// <param name=\"sectionName\">Section ID.</param>\n        /// <returns></returns>\n        private static string GetObjectID(string value, string sectionName)\n        {\n            if (sectionName != \"Terrain\")\n            {\n                string[] splitValue = value.Split(',');\n                if (splitValue.Length < 2) return \"N/A\";\n                else return splitValue[1];\n            }\n            else\n                return value;\n        }\n\n        /// <summary>\n        /// Get key/value pairs from ini file section.\n        /// </summary>\n        /// <param name=\"iniFile\">Ini file.</param>\n        /// <param name=\"sectionName\">Ini file section.</param>\n        /// <returns>List of key/value pairs from the chosen ini file section. If ini file section has no keys, an empty list is returned.</returns>\n        private static List<KeyValuePair<string, string>> GetKeyValuePairs(IniFile iniFile, string sectionName)\n        {\n            IniSection section = iniFile.GetSection(sectionName);\n            if (section == null)\n                return new List<KeyValuePair<string, string>>();\n            return section.Keys;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs",
    "content": "using ClientCore;\nusing DTAClient.Domain.Multiplayer;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Microsoft.Xna.Framework.Input;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing ClientGUI;\nusing ClientCore.Extensions;\nusing System.Diagnostics;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Image = SixLabors.ImageSharp.Image;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    struct MapPreviewBoxExtraMapPreviewTexture\n    {\n        public Texture2D Texture;\n        public Point ControlAreaPoint;\n        public bool Toggleable;\n\n        public MapPreviewBoxExtraMapPreviewTexture(Texture2D texture, Point point, bool toggleable)\n        {\n            Texture = texture;\n            ControlAreaPoint = point;\n            Toggleable = toggleable;\n        }\n    }\n\n    /// <summary>\n    /// The picture box for displaying the map preview.\n    /// </summary>\n    public class MapPreviewBox : XNAPanel, ICompositeControl\n    {\n        public IReadOnlyList<XNAControl> SubControls => [CoopBriefingBox];\n\n        private const int MAX_STARTING_LOCATIONS = 8;\n\n        public delegate void LocalStartingLocationSelectedEventHandler(object sender,\n            LocalStartingLocationEventArgs e);\n\n        public event EventHandler<LocalStartingLocationEventArgs> LocalStartingLocationSelected;\n\n        public event EventHandler StartingLocationApplied;\n\n        private readonly MapLoader mapLoader;\n\n        public MapPreviewBox(WindowManager windowManager, MapLoader mapLoader) : base(windowManager)\n        {\n            PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            FontIndex = 1;\n\n            CoopBriefingBox = new CoopBriefingBox(windowManager);\n            CoopBriefingBox.DrawOrder = 100;  // not a magic number, just high enough so don't need to change it later\n            CoopBriefingBox.UpdateOrder = 100;\n            CoopBriefingBox.Disable();\n\n            NameChanged += MapPreviewBox_NameChanged;\n\n            this.mapLoader = mapLoader;\n        }\n\n        private void MapPreviewBox_NameChanged(object sender, EventArgs e)\n        {\n            CoopBriefingBox.Name = $\"{Name}_CoopBriefingBox\";\n        }\n\n        public void SetFields(List<PlayerInfo> players, List<PlayerInfo> aiPlayers, List<MultiplayerColor> mpColors, string[] sides, IniFile gameOptionsIni)\n        {\n            this.players = players;\n            this.aiPlayers = aiPlayers;\n            this.mpColors = mpColors;\n            this.sides = sides;\n            this.gameOptionsIni = gameOptionsIni;\n\n            Color nameBackgroundColor = AssetLoader.GetRGBAColorFromString(\n                ClientConfiguration.Instance.MapPreviewNameBackgroundColor);\n\n            Color nameBorderColor = AssetLoader.GetRGBAColorFromString(\n                ClientConfiguration.Instance.MapPreviewNameBorderColor);\n\n            double angularVelocity = gameOptionsIni.GetDoubleValue(\"General\", \"StartingLocationAngularVelocity\", 0.015);\n            double reservedAngularVelocity = gameOptionsIni.GetDoubleValue(\"General\", \"ReservedStartingLocationAngularVelocity\", -0.0075);\n\n            Color hoverRemapColor = AssetLoader.GetRGBAColorFromString(ClientConfiguration.Instance.MapPreviewStartingLocationHoverRemapColor);\n\n            startingLocationIndicators = new PlayerLocationIndicator[MAX_STARTING_LOCATIONS];\n            // Init starting location indicators\n            for (int i = 0; i < MAX_STARTING_LOCATIONS; i++)\n            {\n                PlayerLocationIndicator indicator = new PlayerLocationIndicator(WindowManager, mpColors,\n                    nameBackgroundColor, nameBorderColor, contextMenu);\n                indicator.FontIndex = FontIndex;\n                indicator.Visible = false;\n                indicator.Enabled = false;\n                indicator.AngularVelocity = angularVelocity;\n                indicator.HoverRemapColor = hoverRemapColor;\n                indicator.ReversedAngularVelocity = reservedAngularVelocity;\n                indicator.WaypointTexture = AssetLoader.LoadTexture(string.Format(\"slocindicator{0}.png\", i + 1));\n                indicator.Tag = i;\n                indicator.LeftClick += Indicator_LeftClick;\n                indicator.RightClick += Indicator_RightClick;\n\n                startingLocationIndicators[i] = indicator;\n\n                AddChild(indicator);\n            }\n\n            ClientRectangleUpdated += (s, e) => UpdateMap();\n        }\n\n\n        private GameModeMap _gameModeMap;\n        public GameModeMap GameModeMap\n        {\n            get => _gameModeMap;\n            set\n            {\n                _gameModeMap = value;\n                UpdateMap();\n            }\n        }\n\n        public int FontIndex { get; set; }\n\n        /// <summary>\n        /// Controls whether the context menu is enabled for this map preview box.\n        /// Skirmish games and online games where the local player is the host should\n        /// set have this set to true.\n        /// </summary>\n        public bool EnableContextMenu { get; set; }\n        public bool EnableStartLocationSelection { get; set; }\n\n        private readonly string[] teamIds = new[] { string.Empty }\n            .Concat(ProgramConstants.TEAMS.Select(team => $\"[{team}]\")).ToArray();\n\n        private string[] sides;\n\n        public int RandomSelectorCount { get; set; }\n\n        private PlayerLocationIndicator[] startingLocationIndicators;\n\n        private List<MultiplayerColor> mpColors;\n        private List<PlayerInfo> players;\n        private List<PlayerInfo> aiPlayers;\n\n        private XNAContextMenu mainContextMenu;\n        private XNAContextMenu contextMenu;\n        private Point lastContextMenuPoint;\n\n        private XNAContextMenu mapContextMenu;\n        private XNAContextMenuItem toggleFavoriteMapItem;\n        private XNAContextMenuItem toggleExtraTexturesItem;\n        private XNAContextMenuItem showInFolderItem;\n        private XNAClientButton btnToggleFavoriteMap;\n        private XNAClientButton btnToggleExtraTextures;\n\n        private CoopBriefingBox CoopBriefingBox;\n\n        private Rectangle textureRectangle;\n\n        /// <summary>\n        /// Indicates whether `mapPreviewTexture` needs to be disposed before loading the next texture.\n        /// </summary>\n        private bool mapPreviewTextureNeedsDispose = false;\n        private Texture2D mapPreviewTexture = null;\n\n        private bool useNearestNeighbour = false;\n\n        private IniFile gameOptionsIni;\n\n        private EnhancedSoundEffect sndClickSound;\n\n        private EnhancedSoundEffect sndDropdownSound;\n\n        private List<MapPreviewBoxExtraMapPreviewTexture> extraTextures = new List<MapPreviewBoxExtraMapPreviewTexture>(0);\n\n        public EventHandler ToggleFavorite;\n\n        public override void Initialize()\n        {\n            EnableStartLocationSelection = true;\n\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n\n            mainContextMenu = new XNAContextMenu(WindowManager);\n            mainContextMenu.Name = nameof(mainContextMenu);\n            mainContextMenu.ClientRectangle = new Rectangle(0, 0, 150, 2);\n            mainContextMenu.Disable();\n            AddChild(mainContextMenu);\n\n            contextMenu = new XNAContextMenu(WindowManager);\n            contextMenu.Tag = -1;\n            contextMenu.ClientRectangle = new Rectangle(0, 0, 150, 2);\n            AddChild(contextMenu);\n            contextMenu.Disable();\n\n            toggleFavoriteMapItem = new XNAContextMenuItem()\n            {\n                Text = \"Add Favorite\".L10N(\"Client:Main:AddFavorite\"),\n                SelectAction = ToggleFavoriteMap,\n                SelectableChecker = () => GameModeMap != null\n            };\n            toggleExtraTexturesItem = new XNAContextMenuItem()\n            {\n                Text = \"Hide Extra Icons\".L10N(\"Client:Main:HideExtraIcons\"),\n                SelectAction = ToggleExtraTextures,\n                SelectableChecker = () => GameModeMap != null,\n                VisibilityChecker = () => extraTextures.Any(x => x.Toggleable)\n            };\n            showInFolderItem = new XNAContextMenuItem()\n            {\n                Text = \"Show in folder\".L10N(\"Client:Main:ShowInFolder\"),\n                SelectAction = ShowInFolder,\n                SelectableChecker = () => GameModeMap != null\n            };\n            mapContextMenu = new XNAContextMenu(WindowManager);\n            mapContextMenu.ClientRectangle = new Rectangle(0, 0, 120, 2);\n            mapContextMenu.AddItem(toggleFavoriteMapItem);\n            mapContextMenu.AddItem(toggleExtraTexturesItem);\n            mapContextMenu.AddItem(showInFolderItem);\n\n            btnToggleFavoriteMap = new XNAClientButton(WindowManager);\n            btnToggleFavoriteMap.IdleTexture = AssetLoader.LoadTexture(\"favInactive.png\");\n            btnToggleFavoriteMap.LeftClick += (sender, args) => ToggleFavorite?.Invoke(sender, args);\n            btnToggleFavoriteMap.ToolTipText = \"Toggle Favorite Map\".L10N(\"Client:Main:ToggleFavoriteMap\");\n\n            btnToggleExtraTextures = new XNAClientButton(WindowManager);\n            btnToggleExtraTextures.IdleTexture = AssetLoader.LoadTexture(\"pvTexturesActive.png\");\n            btnToggleExtraTextures.LeftClick += (sender, args) => ToggleExtraTextures();\n            btnToggleExtraTextures.ToolTipText = \"Toggle Extra Icons\".L10N(\"Client:Main:ToggleExtraIcons\");\n            btnToggleExtraTextures.Disable();\n\n            AddChild(mapContextMenu);\n            mapContextMenu.Disable();\n\n            // this is needed for the control composition to work properly, as otherwise\n            // the controls will be initialized twice via INItializableWindow system\n            AddChildWithoutInitialize(CoopBriefingBox);\n\n            sndClickSound = new EnhancedSoundEffect(\"button.wav\");\n\n            sndDropdownSound = new EnhancedSoundEffect(\"dropdown.wav\");\n\n            base.Initialize();\n\n            ClientRectangleUpdated += (s, e) => UpdateMap();\n\n            RightClick += MapPreviewBox_RightClick;\n\n            AddChild(btnToggleFavoriteMap);\n            AddChild(btnToggleExtraTextures);\n        }\n\n        private void MapPreviewBox_RightClick(object sender, EventArgs e)\n        {\n            if (GameModeMap == null)\n                return;\n\n            toggleFavoriteMapItem.Text = GameModeMap.IsFavorite ? \"Remove Favorite\".L10N(\"Client:Main:RemoveFavorite\") : \"Add Favorite\".L10N(\"Client:Main:AddFavorite\");\n            toggleExtraTexturesItem.Text = UserINISettings.Instance.DisplayToggleableExtraTextures ?\n                \"Hide Extra Icons\".L10N(\"Client:Main:HideExtraIcons\") : \"Show Extra Icons\".L10N(\"Client:Main:ShowExtraIcons\");\n\n            mapContextMenu.Open(GetCursorPoint());\n        }\n\n        private void ToggleFavoriteMap()\n        {\n            ToggleFavorite?.Invoke(null, null);\n        }\n\n        private void ToggleExtraTextures()\n        {\n            UserINISettings.Instance.DisplayToggleableExtraTextures.Value =\n                !UserINISettings.Instance.DisplayToggleableExtraTextures;\n\n            RefreshExtraTexturesBtn();\n        }\n\n        private void ShowInFolder() => GameModeMap?.Map.OpenContainingFolder();\n\n        private void ContextMenu_OptionSelected(int index)\n        {\n            SoundPlayer.Play(sndDropdownSound);\n\n            if (GameModeMap.EnforceMaxPlayers)\n            {\n                foreach (PlayerInfo pInfo in players.Concat(aiPlayers))\n                {\n                    if (pInfo.StartingLocation == (int)contextMenu.Tag + 1)\n                        pInfo.StartingLocation = 0;\n                }\n            }\n\n            PlayerInfo player;\n\n            if (index >= players.Count)\n            {\n                int aiIndex = index - players.Count;\n                if (aiIndex >= aiPlayers.Count)\n                    return;\n\n                player = aiPlayers[aiIndex];\n            }\n            else\n                player = players[index];\n\n            player.StartingLocation = (int)contextMenu.Tag + 1;\n\n            StartingLocationApplied?.Invoke(this, EventArgs.Empty);\n        }\n\n        /// <summary>\n        /// Allows the user to select their starting location by clicking on one of them\n        /// in the map preview.\n        /// </summary>\n        private void Indicator_LeftClick(object sender, EventArgs e)\n        {\n            if (!EnableStartLocationSelection) return;\n\n            var indicator = (PlayerLocationIndicator)sender;\n\n            SoundPlayer.Play(sndClickSound);\n\n            if (!EnableContextMenu)\n            {\n                if (GameModeMap.EnforceMaxPlayers)\n                {\n                    foreach (PlayerInfo pInfo in players.Concat(aiPlayers))\n                    {\n                        if (pInfo.StartingLocation == (int)indicator.Tag + 1)\n                            return;\n                    }\n                }\n\n                LocalStartingLocationSelected?.Invoke(this, new LocalStartingLocationEventArgs((int)indicator.Tag + 1));\n                return;\n            }\n\n            //if (contextMenu.Visible)\n            //{\n            //    contextMenu.Visible = false;\n            //    contextMenu.Enabled = false;\n            //    return;\n            //}\n\n            //if (Map.EnforceMaxPlayers)\n            //{\n            //    foreach (PlayerInfo pInfo in players.Concat(aiPlayers))\n            //    {\n            //        if (pInfo.StartingLocation == (int)indicator.Tag + 1)\n            //            return;\n            //    }\n            //}\n\n            int x = indicator.Right;\n            int y = indicator.Y;\n\n            if (x + contextMenu.Width > Width)\n                x = indicator.X - contextMenu.Width;\n\n            if (y + contextMenu.Height > Height)\n                y = Height - contextMenu.Height;\n\n            contextMenu.Tag = indicator.Tag;\n\n            int index = 0;\n            foreach (PlayerInfo pInfo in players.Concat(aiPlayers))\n            {\n                contextMenu.Items[index].Selectable = pInfo.StartingLocation != (int)indicator.Tag + 1 &&\n                    pInfo.SideId < sides.Length + RandomSelectorCount;\n                index++;\n            }\n            lastContextMenuPoint = new Point(x, y);\n            contextMenu.Open(lastContextMenuPoint);\n        }\n\n        private void Indicator_RightClick(object sender, EventArgs e)\n        {\n            var indicator = (PlayerLocationIndicator)sender;\n\n            if (!EnableContextMenu)\n            {\n                PlayerInfo pInfo = players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n\n                if (pInfo.StartingLocation == (int)indicator.Tag + 1)\n                {\n                    LocalStartingLocationSelected?.Invoke(this, new LocalStartingLocationEventArgs(0));\n                }\n\n                return;\n            }\n\n            foreach (PlayerInfo pInfo in players.Union(aiPlayers))\n            {\n                if (pInfo.StartingLocation == (int)indicator.Tag + 1)\n                    pInfo.StartingLocation = 0;\n            }\n\n            StartingLocationApplied?.Invoke(this, EventArgs.Empty);\n        }\n\n        /// <summary>\n        /// Updates the map preview texture's position inside\n        /// this control's display rectangle and the \n        /// starting location indicators' positions.\n        /// </summary>\n        private void UpdateMap()\n        {\n            if (mapPreviewTextureNeedsDispose && mapPreviewTexture != null && !mapPreviewTexture.IsDisposed)\n            {\n                mapPreviewTexture.Dispose();\n                mapPreviewTextureNeedsDispose = false;\n            }\n\n            extraTextures.Clear();\n\n            if (GameModeMap == null)\n            {\n                mapPreviewTexture = null;\n                CoopBriefingBox.Disable();\n\n                contextMenu.Disable();\n\n                foreach (var indicator in startingLocationIndicators)\n                    indicator.Disable();\n\n                return;\n            }\n\n            Debug.Assert(!mapPreviewTextureNeedsDispose, \"previous texture must be disposed before loading a new texture\");\n\n            Image previewTextureImage = mapLoader.GetCachedPreviewImageFromMap(GameModeMap.Map, syncLoadOnCacheMiss: true);\n\n            mapPreviewTexture = previewTextureImage != null\n                ? AssetLoader.TextureFromImage(previewTextureImage)\n                // 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.\n                // Use the same `- 2` to let xRatio and yRatio get calculated as 1.\n                : AssetLoader.CreateTexture(Color.Black, Width - 2, Height - 2);\n\n            mapPreviewTextureNeedsDispose = true;\n\n            if (!string.IsNullOrEmpty(GameModeMap.Map.Briefing))\n            {\n                CoopBriefingBox.SetText(GameModeMap.Map.Briefing);\n                CoopBriefingBox.Enable();\n                if (IsActive)\n                    CoopBriefingBox.SetAlpha(0f);\n            }\n            else\n                CoopBriefingBox.Disable();\n\n            double xRatio = (Width - 2) / (double)mapPreviewTexture.Width;\n            double yRatio = (Height - 2) / (double)mapPreviewTexture.Height;\n\n            double ratio;\n\n            int texturePositionX = 1;\n            int texturePositionY = 1;\n            int textureHeight = 0;\n            int textureWidth = 0;\n\n            if (xRatio > yRatio)\n            {\n                ratio = yRatio;\n                textureHeight = Height - 2;\n                textureWidth = (int)(mapPreviewTexture.Width * ratio);\n                texturePositionX = (int)(Width - 2 - textureWidth) / 2;\n            }\n            else\n            {\n                ratio = xRatio;\n                textureWidth = Width - 2;\n                textureHeight = (int)(mapPreviewTexture.Height * ratio);\n                texturePositionY = (Height - 2 - textureHeight) / 2 + 1;\n            }\n\n            useNearestNeighbour = ratio < 1.0;\n\n            textureRectangle = new Rectangle(texturePositionX, texturePositionY,\n                textureWidth, textureHeight);\n\n            List<Point> startingLocations = GameModeMap.Map.GetStartingLocationPreviewCoords(new Point(mapPreviewTexture.Width, mapPreviewTexture.Height));\n\n            // Disable all indicators to be able updated after changing\n            // locations when 2 or more of them have same location (RA1 specifics)\n            foreach (var indicator in startingLocationIndicators)\n            {\n                indicator.Disable();\n            }\n\n            for (int i = 0; i < MAX_STARTING_LOCATIONS; i++)\n            {\n                bool showLocation = i < startingLocations.Count && GameModeMap.AllowedStartingLocations.Contains(i + 1);\n                if (showLocation)\n                {\n                    PlayerLocationIndicator indicator = startingLocationIndicators[i];\n\n                    Point location = new Point(\n                        texturePositionX + (int)(startingLocations[i].X * ratio),\n                        texturePositionY + (int)(startingLocations[i].Y * ratio));\n\n                    indicator.SetPosition(location);\n                    indicator.Enabled = true;\n                    indicator.Visible = true;\n                }\n                else\n                {\n                    startingLocationIndicators[i].Disable();\n                }\n            }\n\n            foreach (ExtraMapPreviewTexture mapExtraTexture in GameModeMap.Map.GetExtraMapPreviewTextures())\n            {\n                // LoadTexture makes use of a texture cache \n                // so we don't need to cache the textures manually\n                Texture2D extraTexture = AssetLoader.LoadTexture(mapExtraTexture.TextureName);\n                Point location = PreviewTexturePointToControlAreaPoint(\n                    GameModeMap.Map.MapPointToMapPreviewPoint(mapExtraTexture.Point,\n                    new Point(mapPreviewTexture.Width - (extraTexture.Width / 2),\n                              mapPreviewTexture.Height - (extraTexture.Height / 2)), mapExtraTexture.Level),\n                              ratio);\n\n                extraTextures.Add(new MapPreviewBoxExtraMapPreviewTexture(extraTexture, location, mapExtraTexture.Toggleable));\n            }\n\n            int buttonX = Width;\n\n            if (extraTextures.Any(x => x.Toggleable))\n            {\n                btnToggleExtraTextures.ClientRectangle = new Rectangle(buttonX - 22, 4, 18, 18);\n                btnToggleExtraTextures.Enable();\n                buttonX = btnToggleExtraTextures.X;\n            }\n            else\n            {\n                btnToggleExtraTextures.Disable();\n            }\n\n            btnToggleFavoriteMap.ClientRectangle = new Rectangle(buttonX - 22, 4, 18, 18);\n\n            RefreshExtraTexturesBtn();\n            RefreshFavoriteBtn();\n        }\n\n        public void RefreshFavoriteBtn()\n        {\n            bool isFav = UserINISettings.Instance.IsFavoriteMap(GameModeMap?.Map.SHA1, GameModeMap?.Map.UntranslatedName, GameModeMap?.GameMode.Name);\n            var textureName = isFav ? \"favActive.png\" : \"favInactive.png\";\n            var hoverTextureName = isFav ? \"favActive_c.png\" : \"favInactive_c.png\";\n            var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null;\n            btnToggleFavoriteMap.IdleTexture = AssetLoader.LoadTexture(textureName);\n            btnToggleFavoriteMap.HoverTexture = hoverTexture;\n        }\n\n\n        public void RefreshExtraTexturesBtn()\n        {\n            var textureName = UserINISettings.Instance.DisplayToggleableExtraTextures ? \"pvTexturesActive.png\" : \"pvTexturesInactive.png\";\n            var hoverTextureName = UserINISettings.Instance.DisplayToggleableExtraTextures ? \"pvTexturesActive_c.png\" : \"pvTexturesInactive_c.png\";\n            var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null;\n            btnToggleExtraTextures.IdleTexture = AssetLoader.LoadTexture(textureName);\n            btnToggleExtraTextures.HoverTexture = hoverTexture;\n        }\n\n        private Point PreviewTexturePointToControlAreaPoint(Point previewTexturePoint, double scaleRatio)\n        {\n            return new Point(textureRectangle.X + (int)(previewTexturePoint.X * scaleRatio),\n                textureRectangle.Y + (int)(previewTexturePoint.Y * scaleRatio));\n        }\n\n        public void UpdateStartingLocationTexts()\n        {\n            foreach (PlayerLocationIndicator indicator in startingLocationIndicators)\n                indicator.Players.Clear();\n\n            foreach (PlayerInfo pInfo in players)\n            {\n                if (pInfo.StartingLocation > 0)\n                    startingLocationIndicators[pInfo.StartingLocation - 1].Players.Add(pInfo);\n            }\n\n            foreach (PlayerInfo aiInfo in aiPlayers)\n            {\n                if (aiInfo.StartingLocation > 0)\n                    startingLocationIndicators[aiInfo.StartingLocation - 1].Players.Add(aiInfo);\n            }\n\n            foreach (PlayerLocationIndicator indicator in startingLocationIndicators)\n                indicator.Refresh();\n\n            contextMenu.ClearItems();\n\n            int id = 1;\n            var playerList = players.Concat(aiPlayers).ToList();\n\n            for (int i = 0; i < playerList.Count; i++)\n            {\n                PlayerInfo pInfo = playerList[i];\n\n                string text = pInfo.Name;\n\n                if (pInfo.TeamId > 0)\n                {\n                    text = teamIds[pInfo.TeamId] + \" \" + text;\n                }\n\n                int index = i;\n                XNAContextMenuItem item = new XNAContextMenuItem()\n                {\n                    Text = id + \". \" + text,\n                    TextColor = pInfo.ColorId > 0 ? mpColors[pInfo.ColorId - 1].XnaColor : Color.White,\n                    SelectAction = () => ContextMenu_OptionSelected(index),\n                };\n                contextMenu.AddItem(item);\n\n                id++;\n            }\n\n            if (EnableContextMenu && contextMenu.Enabled && contextMenu.Visible)\n            {\n                contextMenu.Disable();\n                contextMenu.Open(lastContextMenuPoint);\n            }\n        }\n\n        public override void OnMouseEnter()\n        {\n            foreach (PlayerLocationIndicator indicator in startingLocationIndicators)\n                indicator.BackgroundShown = true;\n\n            if (GameModeMap != null && !string.IsNullOrEmpty(GameModeMap.Map.Briefing))\n            {\n                CoopBriefingBox.SetFadeVisibility(false);\n            }\n            else\n                CoopBriefingBox.Disable();\n\n            base.OnMouseEnter();\n        }\n\n        public override void OnMouseLeave()\n        {\n            foreach (PlayerLocationIndicator indicator in startingLocationIndicators)\n                indicator.BackgroundShown = false;\n\n            if (GameModeMap != null && !string.IsNullOrEmpty(GameModeMap.Map.Briefing))\n            {\n                CoopBriefingBox.SetText(GameModeMap.Map.Briefing);\n                CoopBriefingBox.SetFadeVisibility(true);\n            }\n\n            base.OnMouseLeave();\n        }\n\n        public override void OnLeftClick(InputEventArgs inputEventArgs)\n        {\n            inputEventArgs.Handled = true;\n\n            if (Keyboard.IsKeyHeldDown(Keys.LeftControl))\n            {\n                FileInfo previewFileInfo = SafePath.GetFile(ProgramConstants.GamePath, GameModeMap.Map.PreviewPath);\n\n                if (previewFileInfo.Exists)\n                {\n                    try\n                    {\n                        ProcessLauncher.StartShellProcess(previewFileInfo.FullName);\n                    }\n                    catch { }\n                }\n            }\n\n            base.OnLeftClick(inputEventArgs);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            DrawPanel();\n\n            if (mapPreviewTexture != null)\n            {\n                Point renderPoint = GetRenderPoint();\n\n                if (useNearestNeighbour)\n                {\n                    Renderer.PushSettings(new SpriteBatchSettings(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null));\n                    DrawPreviewTexture(renderPoint);\n                    Renderer.PopSettings();\n                }\n                else\n                {\n                    DrawPreviewTexture(renderPoint);\n                }\n\n                if (DrawBorders)\n                    DrawPanelBorders();\n\n                foreach (var extraTexture in extraTextures)\n                {\n                    if (!extraTexture.Toggleable || UserINISettings.Instance.DisplayToggleableExtraTextures)\n                    {\n                        Renderer.DrawTexture(extraTexture.Texture,\n                            new Rectangle(renderPoint.X + extraTexture.ControlAreaPoint.X,\n                            renderPoint.Y + extraTexture.ControlAreaPoint.Y,\n                            extraTexture.Texture.Width, extraTexture.Texture.Height), Color.White);\n                    }\n                }\n            }\n            else if (DrawBorders)\n            {\n                DrawPanelBorders();\n            }\n\n            DrawChildren(gameTime);\n        }\n\n        private void DrawPreviewTexture(Point renderPoint)\n        {\n            Renderer.DrawTexture(mapPreviewTexture,\n                new Rectangle(renderPoint.X + textureRectangle.X,\n                renderPoint.Y + textureRectangle.Y,\n                textureRectangle.Width, textureRectangle.Height),\n                Color.White);\n        }\n    }\n\n    public class LocalStartingLocationEventArgs : EventArgs\n    {\n        public LocalStartingLocationEventArgs(int startingLocationIndex)\n        {\n            StartingLocationIndex = startingLocationIndex;\n        }\n\n        public int StartingLocationIndex { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing Microsoft.Xna.Framework;\nusing ClientCore;\nusing System.IO;\nusing Rampastring.Tools;\nusing ClientCore.Statistics;\nusing DTAClient.DXGUI.Generic;\nusing DTAClient.Domain.Multiplayer;\nusing ClientGUI;\nusing System.Text;\nusing DTAClient.Domain;\nusing Microsoft.Xna.Framework.Graphics;\nusing ClientCore.Extensions;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing System.Diagnostics;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    /// <summary>\n    /// A generic base class for multiplayer game lobbies (CnCNet and LAN).\n    /// </summary>\n    public abstract class MultiplayerGameLobby : GameLobbyBase, ISwitchable\n    {\n        private const int MAX_DICE = 10;\n        private const int MAX_DIE_SIDES = 100;\n\n        public MultiplayerGameLobby(WindowManager windowManager, string iniName,\n            TopBar topBar, MapLoader mapLoader, DiscordHandler discordHandler, PrivateMessagingWindow pmWindow, Random random)\n            : base(windowManager, iniName, mapLoader, true, discordHandler, random)\n        {\n            TopBar = topBar;\n            this.random = random;\n\n            chatBoxCommands = new List<ChatBoxCommand>\n            {\n                new ChatBoxCommand(\"HIDEMAPS\", \"Hide map list (game host only)\".L10N(\"Client:Main:ChatboxCommandHideMapsHelp\"), true,\n                    s => HideMapList()),\n                new ChatBoxCommand(\"SHOWMAPS\", \"Show map list (game host only)\".L10N(\"Client:Main:ChatboxCommandShowMapsHelp\"), true,\n                    s => ShowMapList()),\n                new ChatBoxCommand(\"FRAMESENDRATE\", string.Format(\"Change order lag / FrameSendRate (default {0}) (game host only)\".L10N(\"Client:Main:ChatboxCommandFrameSendRateHelpV2\"), ClientConfiguration.Instance.DefaultFrameSendRate), true,\n                    s => SetFrameSendRate(s)),\n                new ChatBoxCommand(\"MAXAHEAD\", string.Format(\"Change MaxAhead (default {0}) (game host only)\".L10N(\"Client:Main:ChatboxCommandMaxAheadHelpV2\"), ClientConfiguration.Instance.DefaultMaxAhead), true,\n                    s => SetMaxAhead(s)),\n                new ChatBoxCommand(\"PROTOCOLVERSION\", string.Format(\"Change ProtocolVersion (default {0}) (game host only)\".L10N(\"Client:Main:ChatboxCommandProtocolVersionHelpV2\"), ClientConfiguration.Instance.DefaultProtocolVersion), true,\n                    s => SetProtocolVersion(s)),\n                new ChatBoxCommand(\"LOADMAP\", \"Load a custom map with given filename from /Maps/Custom/ folder.\".L10N(\"Client:Main:ChatboxCommandLoadMapHelp\"), true, LoadCustomMap),\n                new ChatBoxCommand(\"RANDOMSTARTS\", \"Enables completely random starting locations (Tiberian Sun based games only).\".L10N(\"Client:Main:ChatboxCommandRandomStartsHelp\"), true,\n                    s => SetStartingLocationClearance(s)),\n                new ChatBoxCommand(\"ROLL\", \"Roll dice, for example /roll 3d6\".L10N(\"Client:Main:ChatboxCommandRollHelp\"), false, RollDiceCommand),\n                new ChatBoxCommand(\"SAVEOPTIONS\", \"Save game option preset so it can be loaded later\".L10N(\"Client:Main:ChatboxCommandSaveOptionsHelp\"), false, HandleGameOptionPresetSaveCommand),\n                new ChatBoxCommand(\"LOADOPTIONS\", \"Load game option preset\".L10N(\"Client:Main:ChatboxCommandLoadOptionsHelp\"), true, HandleGameOptionPresetLoadCommand)\n            };\n        }\n\n        protected XNAPlayerSlotIndicator[] StatusIndicators;\n\n        protected ChatListBox lbChatMessages;\n        protected XNAChatTextBox tbChatInput;\n        protected XNAClientButton btnLockGame;\n        protected XNAClientCheckBox chkAutoReady;\n\n        private Random random;\n\n        protected bool IsHost = false;\n\n        private bool locked = false;\n        protected bool Locked\n        {\n            get => locked;\n            set\n            {\n                bool oldLocked = locked;\n                locked = value;\n                if (oldLocked != value)\n                {\n                    CopyPlayerDataToUI();\n                    UpdateDiscordPresence();\n                }\n            }\n        }\n\n        // protected bool DisableSpectatorReadyChecking = false;\n\n        protected EnhancedSoundEffect sndJoinSound;\n        protected EnhancedSoundEffect sndLeaveSound;\n        protected EnhancedSoundEffect sndMessageSound;\n        protected EnhancedSoundEffect sndGetReadySound;\n        protected EnhancedSoundEffect sndReturnSound;\n\n        protected Texture2D[] PingTextures;\n\n        protected TopBar TopBar;\n\n        protected int FrameSendRate { get; set; }\n\n        /// <summary>\n        /// Controls the MaxAhead parameter. The default value of 0 means that \n        /// the value is not written to spawn.ini, which allows the spawner the\n        /// calculate and assign the MaxAhead value.\n        /// </summary>\n        protected int MaxAhead { get; set; }\n\n        protected int ProtocolVersion { get; set; }\n\n        protected List<ChatBoxCommand> chatBoxCommands;\n\n        private FileSystemWatcher fsw;\n\n        private bool gameSaved = false;\n\n        protected bool LastMapChangeWasInvalid { get; set; } = false;\n\n        /// <summary>\n        /// Allows derived classes to add their own chat box commands.\n        /// </summary>\n        /// <param name=\"command\">The command to add.</param>\n        protected void AddChatBoxCommand(ChatBoxCommand command) => chatBoxCommands.Add(command);\n\n        public override void Initialize()\n        {\n            Name = nameof(MultiplayerGameLobby);\n\n            base.Initialize();\n\n            // Init default game network settings\n            FrameSendRate = ClientConfiguration.Instance.DefaultFrameSendRate;\n            ProtocolVersion = ClientConfiguration.Instance.DefaultProtocolVersion;\n            MaxAhead = ClientConfiguration.Instance.DefaultMaxAhead;\n\n            // DisableSpectatorReadyChecking = GameOptionsIni.GetBooleanValue(\"General\", \"DisableSpectatorReadyChecking\", false);\n\n            PingTextures = new Texture2D[5]\n            {\n                AssetLoader.LoadTexture(\"ping0.png\"),\n                AssetLoader.LoadTexture(\"ping1.png\"),\n                AssetLoader.LoadTexture(\"ping2.png\"),\n                AssetLoader.LoadTexture(\"ping3.png\"),\n                AssetLoader.LoadTexture(\"ping4.png\")\n            };\n\n            InitPlayerOptionDropdowns();\n\n            StatusIndicators = new XNAPlayerSlotIndicator[MAX_PLAYER_COUNT];\n\n            int statusIndicatorX = ConfigIni.GetIntValue(Name, \"PlayerStatusIndicatorX\", 0);\n            int statusIndicatorY = ConfigIni.GetIntValue(Name, \"PlayerStatusIndicatorY\", 0);\n\n            for (int i = 0; i < MAX_PLAYER_COUNT; i++)\n            {\n                var indicatorPlayerReady = new XNAPlayerSlotIndicator(WindowManager);\n                indicatorPlayerReady.Name = \"playerStatusIndicator\" + i;\n                indicatorPlayerReady.ClientRectangle = new Rectangle(statusIndicatorX, ddPlayerTeams[i].Y + statusIndicatorY,\n                    0, 0);\n\n                PlayerOptionsPanel.AddChild(indicatorPlayerReady);\n\n                StatusIndicators[i] = indicatorPlayerReady;\n\n                const string spectatorName = \"Spectator\";\n                AddSideToDropDown(ddPlayerSides[i], spectatorName, spectatorName.L10N(\"Client:Sides:SpectatorSide\"), AssetLoader.LoadTexture(\"spectatoricon.png\"));\n            }\n\n            lbChatMessages = FindChild<ChatListBox>(nameof(lbChatMessages));\n\n            tbChatInput = FindChild<XNAChatTextBox>(nameof(tbChatInput));\n            tbChatInput.MaximumTextLength = 150;\n            tbChatInput.EnterPressed += TbChatInput_EnterPressed;\n\n            btnLockGame = FindChild<XNAClientButton>(nameof(btnLockGame));\n            btnLockGame.LeftClick += BtnLockGame_LeftClick;\n\n            chkAutoReady = FindChild<XNAClientCheckBox>(nameof(chkAutoReady));\n            chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged;\n            chkAutoReady.Disable();\n\n            MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected;\n            MapPreviewBox.StartingLocationApplied += MapPreviewBox_StartingLocationApplied;\n\n            sndJoinSound = new EnhancedSoundEffect(\"joingame.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown);\n            sndLeaveSound = new EnhancedSoundEffect(\"leavegame.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown);\n            sndMessageSound = new EnhancedSoundEffect(\"message.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown);\n            sndGetReadySound = new EnhancedSoundEffect(\"getready.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyGetReadyCooldown);\n            sndReturnSound = new EnhancedSoundEffect(\"return.wav\", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyReturnCooldown);\n\n            if (SavedGameManager.AreSavedGamesAvailable())\n            {\n                fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, \"Saved Games\"), \"*.NET\");\n                fsw.Created += fsw_Created;\n                fsw.Changed += fsw_Created;\n                fsw.EnableRaisingEvents = false;\n            }\n            else\n            {\n                Logger.Log(\"MultiplayerGameLobby: Saved games are not available!\");\n            }\n\n            ParseHostPlayerControls();\n        }\n\n        /// <summary>\n        /// Reads INI for host/player variations of controls and restores it back.\n        /// </summary>\n        /// <remarks>\n        /// Needed for translation notification mechanism to work correctly.\n        /// </remarks>\n        private void ParseHostPlayerControls()\n        {\n            string temp = lbChatMessages.Name;\n\n            lbChatMessages.Name = \"lbChatMessages_Host\";\n            ReadINIForControl(lbChatMessages);\n            lbChatMessages.Name = \"lbChatMessages_Player\";\n            ReadINIForControl(lbChatMessages);\n            lbChatMessages.Name = temp;\n            ReadINIForControl(lbChatMessages);\n\n            temp = tbChatInput.Name;\n\n            tbChatInput.Name = \"tbChatInput_Host\";\n            ReadINIForControl(tbChatInput);\n            tbChatInput.Name = \"tbChatInput_Player\";\n            ReadINIForControl(tbChatInput);\n            tbChatInput.Name = temp;\n            ReadINIForControl(tbChatInput);\n        }\n\n        /// <summary>\n        /// Performs initialization that is necessary after derived\n        /// classes have performed their own initialization.\n        /// </summary>\n        protected void PostInitialize()\n        {\n            CenterOnParent();\n            LoadDefaultGameModeMap();\n        }\n\n        private void fsw_Created(object sender, FileSystemEventArgs e)\n        {\n            AddCallback(new Action<FileSystemEventArgs>(FSWEvent), e);\n        }\n\n        private void FSWEvent(FileSystemEventArgs e)\n        {\n            Logger.Log(\"FSW Event: \" + e.FullPath);\n\n            if (Path.GetFileName(e.FullPath) == \"SAVEGAME.NET\")\n            {\n                if (!gameSaved)\n                {\n                    bool success = SavedGameManager.InitSavedGames();\n\n                    if (!success)\n                        return;\n                }\n\n                gameSaved = true;\n\n                SavedGameManager.RenameSavedGame();\n            }\n        }\n\n        protected override void StartGame()\n        {\n            if (fsw != null)\n                fsw.EnableRaisingEvents = true;\n\n            if (UserINISettings.Instance.StopGameLobbyMessageAudio)\n                sndMessageSound.Enabled = false;\n\n            base.StartGame();\n        }\n\n        protected override void GameProcessExited()\n        {\n            gameSaved = false;\n\n            if (fsw != null)\n                fsw.EnableRaisingEvents = false;\n\n            PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n            pInfo.IsInGame = false;\n\n            if (UserINISettings.Instance.StopGameLobbyMessageAudio)\n                sndMessageSound.Enabled = true;\n\n            base.GameProcessExited();\n\n            if (IsHost)\n            {\n                GenerateGameID();\n                DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks\n            }\n            else if (chkAutoReady.Checked)\n            {\n                RequestReadyStatus();\n            }\n        }\n\n        private void GenerateGameID()\n        {\n            int i = 0;\n\n            while (i < 20)\n            {\n                string s = DateTime.Now.Day.ToString() +\n                    DateTime.Now.Month.ToString() +\n                    DateTime.Now.Hour.ToString() +\n                    DateTime.Now.Minute.ToString();\n\n                UniqueGameID = int.Parse(i.ToString() + s);\n\n                if (StatisticsManager.Instance.GetMatchWithGameID(UniqueGameID) == null)\n                    break;\n\n                i++;\n            }\n        }\n\n        private void BtnLockGame_LeftClick(object sender, EventArgs e)\n        {\n            HandleLockGameButtonClick();\n        }\n\n        protected virtual void HandleLockGameButtonClick()\n        {\n            if (Locked)\n                UnlockGame(true);\n            else\n                LockGame();\n        }\n\n        protected abstract void LockGame();\n\n        protected abstract void UnlockGame(bool manual);\n\n        private void TbChatInput_EnterPressed(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbChatInput.Text))\n                return;\n\n            if (tbChatInput.Text.StartsWith(\"/\"))\n            {\n                string text = tbChatInput.Text;\n                string command;\n                string parameters;\n\n                int spaceIndex = text.IndexOf(' ');\n\n                if (spaceIndex == -1)\n                {\n                    command = text.Substring(1).ToUpper();\n                    parameters = string.Empty;\n                }\n                else\n                {\n                    command = text.Substring(1, spaceIndex - 1);\n                    parameters = text.Substring(spaceIndex + 1);\n                }\n\n                tbChatInput.Text = string.Empty;\n\n                foreach (var chatBoxCommand in chatBoxCommands)\n                {\n                    if (command.ToUpper() == chatBoxCommand.Command)\n                    {\n                        if (!IsHost && chatBoxCommand.HostOnly)\n                        {\n                            AddNotice(string.Format(\"/{0} is for game hosts only.\".L10N(\"Client:Main:ChatboxCommandHostOnly\"), chatBoxCommand.Command));\n                            return;\n                        }\n\n                        chatBoxCommand.Action(parameters);\n                        return;\n                    }\n                }\n\n                StringBuilder sb = new StringBuilder(\"To use a command, start your message with /<command>. Possible chat box commands:\".L10N(\"Client:Main:ChatboxCommandTipText\") + \" \");\n                foreach (var chatBoxCommand in chatBoxCommands)\n                {\n                    sb.Append(Environment.NewLine);\n                    sb.Append(Environment.NewLine);\n                    sb.Append($\"{chatBoxCommand.Command}: {chatBoxCommand.Description}\");\n                }\n                XNAMessageBox.Show(WindowManager, \"Chat Box Command Help\".L10N(\"Client:Main:ChatboxCommandTipTitle\"), sb.ToString());\n                return;\n            }\n\n            SendChatMessage(tbChatInput.Text);\n            tbChatInput.Text = string.Empty;\n        }\n\n        private void ChkAutoReady_CheckedChanged(object sender, EventArgs e)\n        {\n            UpdateLaunchGameButtonStatus();\n            RequestReadyStatus();\n        }\n\n        protected void ResetAutoReadyCheckbox()\n        {\n            chkAutoReady.CheckedChanged -= ChkAutoReady_CheckedChanged;\n            chkAutoReady.Checked = false;\n            chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged;\n            UpdateLaunchGameButtonStatus();\n        }\n\n        private void SetFrameSendRate(string value)\n        {\n            bool success = int.TryParse(value, out int intValue);\n\n            if (!success)\n            {\n                AddNotice(\"Command syntax: /FrameSendRate <number>\".L10N(\"Client:Main:ChatboxCommandFrameSendRateSyntax\"));\n                return;\n            }\n\n            FrameSendRate = intValue;\n            AddNotice(string.Format(\"FrameSendRate has been changed to {0}\".L10N(\"Client:Main:FrameSendRateChanged\"), intValue));\n\n            OnGameOptionChanged();\n            ClearReadyStatuses();\n        }\n\n        private void SetMaxAhead(string value)\n        {\n            bool success = int.TryParse(value, out int intValue);\n\n            if (!success)\n            {\n                AddNotice(\"Command syntax: /MaxAhead <number>\".L10N(\"Client:Main:ChatboxCommandMaxAheadSyntax\"));\n                return;\n            }\n\n            MaxAhead = intValue;\n            AddNotice(string.Format(\"MaxAhead has been changed to {0}\".L10N(\"Client:Main:MaxAheadChanged\"), intValue));\n\n            OnGameOptionChanged();\n            ClearReadyStatuses();\n        }\n\n        private void SetProtocolVersion(string value)\n        {\n            bool success = int.TryParse(value, out int intValue);\n\n            if (!success)\n            {\n                AddNotice(\"Command syntax: /ProtocolVersion <number>.\".L10N(\"Client:Main:ChatboxCommandProtocolVersionSyntax\"));\n                return;\n            }\n\n            if (!(intValue == 0 || intValue == 2))\n            {\n                AddNotice(\"ProtocolVersion only allows values 0 and 2.\".L10N(\"Client:Main:ChatboxCommandProtocolVersionInvalid\"));\n                return;\n            }\n\n            ProtocolVersion = intValue;\n            AddNotice(string.Format(\"ProtocolVersion has been changed to {0}\".L10N(\"Client:Main:ProtocolVersionChanged\"), intValue));\n\n            OnGameOptionChanged();\n            ClearReadyStatuses();\n        }\n\n        private void SetStartingLocationClearance(string value)\n        {\n            bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations);\n\n            SetRandomStartingLocations(removeStartingLocations);\n\n            OnGameOptionChanged();\n            ClearReadyStatuses();\n        }\n\n        /// <summary>\n        /// Enables or disables completely random starting locations and informs\n        /// the user accordingly.\n        /// </summary>\n        /// <param name=\"newValue\">The new value of completely random starting locations.</param>\n        protected void SetRandomStartingLocations(bool newValue)\n        {\n            if (newValue != RemoveStartingLocations)\n            {\n                RemoveStartingLocations = newValue;\n                if (RemoveStartingLocations)\n                    AddNotice(\"The game host has enabled completely random starting locations (only works for regular maps).\".L10N(\"Client:Main:HostEnabledRandomStartLocation\"));\n                else\n                    AddNotice(\"The game host has disabled completely random starting locations.\".L10N(\"Client:Main:HostDisabledRandomStartLocation\"));\n            }\n        }\n\n        /// <summary>\n        /// Handles the dice rolling command.\n        /// </summary>\n        /// <param name=\"dieType\">The parameters given for the command by the user.</param>\n        private void RollDiceCommand(string dieType)\n        {\n            int dieSides = 6;\n            int dieCount = 1;\n\n            if (!string.IsNullOrEmpty(dieType))\n            {\n                string[] parts = dieType.Split('d');\n                if (parts.Length == 2)\n                {\n                    if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides))\n                    {\n                        AddNotice(\"Invalid dice specified. Expected format: /roll <die count>d<die sides>\".L10N(\"Client:Main:ChatboxCommandRollInvalidAndSyntax\"));\n                        return;\n                    }\n                }\n            }\n\n            if (dieCount > MAX_DICE || dieCount < 1)\n            {\n                AddNotice(\"You can only between 1 to 10 dies at once.\".L10N(\"Client:Main:ChatboxCommandRollInvalid2\"));\n                return;\n            }\n\n            if (dieSides > MAX_DIE_SIDES || dieSides < 2)\n            {\n                AddNotice(\"You can only have between 2 and 100 sides in a die.\".L10N(\"Client:Main:ChatboxCommandRollInvalid3\"));\n                return;\n            }\n\n            int[] results = new int[dieCount];\n            for (int i = 0; i < dieCount; i++)\n            {\n                results[i] = random.Next(1, dieSides + 1);\n            }\n\n            BroadcastDiceRoll(dieSides, results);\n        }\n\n        /// <summary>\n        /// Handles custom map load command.\n        /// </summary>\n        /// <param name=\"mapName\">Name of the map given as a parameter, without file extension.</param>\n        private void LoadCustomMap(string mapName)\n        {\n            Map map = MapLoader.LoadCustomMap($\"Maps/Custom/{mapName}\", out string resultMessage);\n            if (map != null)\n            {\n                AddNotice(resultMessage);\n                ListMaps();\n            }\n            else\n            {\n                AddNotice(resultMessage, Color.Red);\n            }\n        }\n\n        /// <summary>\n        /// Override in derived classes to broadcast the results of rolling dice to other players.\n        /// </summary>\n        /// <param name=\"dieSides\">The number of sides in the dice.</param>\n        /// <param name=\"results\">The results of the dice roll.</param>\n        protected abstract void BroadcastDiceRoll(int dieSides, int[] results);\n\n        /// <summary>\n        /// Parses and lists the results of rolling dice.\n        /// </summary>\n        /// <param name=\"senderName\">The player that rolled the dice.</param>\n        /// <param name=\"result\">The results of rolling dice, with each die separated by a comma\n        /// and the number of sides in the die included as the first number.</param>\n        /// <example>\n        /// HandleDiceRollResult(\"Rampastring\", \"6,3,5,1\") would mean that\n        /// Rampastring rolled three six-sided dice and got 3, 5 and 1.\n        /// </example>\n        protected void HandleDiceRollResult(string senderName, string result)\n        {\n            if (string.IsNullOrEmpty(result))\n                return;\n\n            string[] parts = result.Split(',');\n            if (parts.Length < 2 || parts.Length > MAX_DICE + 1)\n                return;\n\n            int[] intArray = Array.ConvertAll(parts, (s) => { return Conversions.IntFromString(s, -1); });\n            int dieSides = intArray[0];\n            if (dieSides < 1 || dieSides > MAX_DIE_SIDES)\n                return;\n            int[] results = new int[intArray.Length - 1];\n            Array.ConstrainedCopy(intArray, 1, results, 0, results.Length);\n\n            for (int i = 1; i < intArray.Length; i++)\n            {\n                if (intArray[i] < 1 || intArray[i] > dieSides)\n                    return;\n            }\n\n            PrintDiceRollResult(senderName, dieSides, results);\n        }\n\n        /// <summary>\n        /// Prints the result of rolling dice.\n        /// </summary>\n        /// <param name=\"senderName\">The player who rolled dice.</param>\n        /// <param name=\"dieSides\">The number of sides in the die.</param>\n        /// <param name=\"results\">The results of the roll.</param>\n        protected void PrintDiceRollResult(string senderName, int dieSides, int[] results)\n        {\n            AddNotice(String.Format(\"{0} rolled {1}d{2} and got {3}\".L10N(\"Client:Main:PrintDiceRollResult\"),\n                senderName, results.Length, dieSides, string.Join(\", \", results)\n            ));\n        }\n\n        protected abstract void SendChatMessage(string message);\n\n        /// <summary>\n        /// Changes the game lobby's UI depending on whether the local player is the host.\n        /// </summary>\n        /// <param name=\"isHost\">Determines whether the local player is the host of the game.</param>\n        protected void Refresh(bool isHost)\n        {\n            IsHost = isHost;\n            Locked = false;\n            CopyPlayerDataToUI();\n\n            UpdateMapPreviewBoxEnabledStatus();\n            PlayerExtraOptionsPanel?.SetIsHost(isHost);\n            //MapPreviewBox.EnableContextMenu = IsHost;\n\n            btnLaunchGame.Text = IsHost ? BTN_LAUNCH_GAME : BTN_LAUNCH_READY;\n\n            if (IsHost)\n            {\n                ShowMapList();\n                btnSaveLoadGameOptions?.Enable();\n\n                btnLockGame.Text = \"Lock Game\".L10N(\"Client:Main:ButtonLockGame\");\n                btnLockGame.Enabled = true;\n                btnLockGame.Visible = true;\n                chkAutoReady.Disable();\n\n                foreach (GameLobbyDropDown dd in DropDowns)\n                {\n                    dd.InputEnabled = true;\n                    dd.SelectedIndex = dd.UserSelectedIndex;\n                }\n\n                foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n                {\n                    checkBox.AllowChanges = true;\n                    checkBox.Checked = checkBox.UserChecked;\n                }\n\n                GenerateGameID();\n            }\n            else\n            {\n                HideMapList();\n                btnSaveLoadGameOptions?.Disable();\n\n                btnLockGame.Enabled = false;\n                btnLockGame.Visible = false;\n                ReadINIForControl(chkAutoReady);\n\n                foreach (GameLobbyDropDown dd in DropDowns)\n                    dd.InputEnabled = false;\n\n                foreach (GameLobbyCheckBox checkBox in CheckBoxes)\n                    checkBox.AllowChanges = false;\n            }\n\n            LoadDefaultGameModeMap();\n\n            lbChatMessages.Clear();\n            lbChatMessages.TopIndex = 0;\n\n            lbChatMessages.AddItem(\"Type / to view a list of available chat commands.\".L10N(\"Client:Main:ChatCommandTip\"), Color.Silver, true);\n\n            if (SavedGameManager.GetSaveGameCount() > 0)\n            {\n                lbChatMessages.AddItem((\"Multiplayer saved games from a previous match have been detected. \" +\n                    \"The saved games of the previous match will be deleted if you create new saves during this match.\").L10N(\"Client:Main:SavedGameDetected\"),\n                    Color.Yellow, true);\n            }\n        }\n\n        private void HideMapList()\n        {\n            lbChatMessages.Name = \"lbChatMessages_Player\";\n            tbChatInput.Name = \"tbChatInput_Player\";\n            MapPreviewBox.Name = \"MapPreviewBox\";\n            lblMapName.Name = \"lblMapName\";\n            lblMapAuthor.Name = \"lblMapAuthor\";\n            lblGameMode.Name = \"lblGameMode\";\n            lblMapSize.Name = \"lblMapSize\";\n\n            ReadINIForControl(btnPickRandomMap);\n            ReadINIForControl(lbChatMessages);\n            ReadINIForControl(tbChatInput);\n            ReadINIForControl(lbGameModeMapList);\n            ReadINIForControl(lblMapName);\n            ReadINIForControl(lblMapAuthor);\n            ReadINIForControl(lblGameMode);\n            ReadINIForControl(lblMapSize);\n            ReadINIForControl(btnMapSortAlphabetically);\n\n            ddGameModeMapFilter.Disable();\n            lblGameModeSelect.Disable();\n            lbGameModeMapList.Disable();\n            tbMapSearch.Disable();\n            btnPickRandomMap.Disable();\n            btnMapSortAlphabetically.Disable();\n\n            SetMapLabels();\n        }\n\n        private void ShowMapList()\n        {\n            lbChatMessages.Name = \"lbChatMessages_Host\";\n            tbChatInput.Name = \"tbChatInput_Host\";\n            MapPreviewBox.Name = \"MapPreviewBox\";\n            lblMapName.Name = \"lblMapName\";\n            lblMapAuthor.Name = \"lblMapAuthor\";\n            lblGameMode.Name = \"lblGameMode\";\n            lblMapSize.Name = \"lblMapSize\";\n\n            ddGameModeMapFilter.Enable();\n            lblGameModeSelect.Enable();\n            lbGameModeMapList.Enable();\n            tbMapSearch.Enable();\n            btnPickRandomMap.Enable();\n            btnMapSortAlphabetically.Enable();\n\n            ReadINIForControl(btnPickRandomMap);\n            ReadINIForControl(lbChatMessages);\n            ReadINIForControl(tbChatInput);\n            ReadINIForControl(lbGameModeMapList);\n            ReadINIForControl(lblMapName);\n            ReadINIForControl(lblMapAuthor);\n            ReadINIForControl(lblGameMode);\n            ReadINIForControl(lblMapSize);\n            ReadINIForControl(btnMapSortAlphabetically);\n\n            SetMapLabels();\n        }\n\n        private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalStartingLocationEventArgs e)\n        {\n            int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME);\n\n            if (mTopIndex == -1 || Players[mTopIndex].SideId == ddPlayerSides[0].Items.Count - 1)\n                return;\n\n            ddPlayerStarts[mTopIndex].SelectedIndex = e.StartingLocationIndex;\n        }\n\n        private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e)\n        {\n            ClearReadyStatuses();\n            CopyPlayerDataToUI();\n            BroadcastPlayerOptions();\n        }\n\n        /// <summary>\n        /// Handles the user's click on the \"Launch Game\" / \"I'm Ready\" button.\n        /// If the local player is the game host, checks if the game can be launched and then\n        /// launches the game if it's allowed. If the local player isn't the game host,\n        /// sends a ready request.\n        /// </summary>\n        protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e)\n        {\n            if (!IsHost)\n            {\n                RequestReadyStatus();\n                return;\n            }\n\n            if (!Locked)\n            {\n                LockGameNotification();\n                return;\n            }\n\n            var teamMappingsError = GetTeamMappingsError();\n            if (!string.IsNullOrEmpty(teamMappingsError))\n            {\n                AddNotice(teamMappingsError);\n                return;\n            }\n\n            List<int> occupiedColorIds = new List<int>();\n            foreach (PlayerInfo player in Players)\n            {\n                if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0)\n                {\n                    SharedColorsNotification();\n                    return;\n                }\n\n                occupiedColorIds.Add(player.ColorId);\n            }\n\n            if (AIPlayers.Count(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1) > 0)\n            {\n                AISpectatorsNotification();\n                return;\n            }\n\n            if (GameModeMap.EnforceMaxPlayers)\n            {\n                foreach (PlayerInfo pInfo in Players)\n                {\n                    if (pInfo.StartingLocation == 0)\n                        continue;\n\n                    if (Players.Concat(AIPlayers).ToList().Find(\n                        p => p.StartingLocation == pInfo.StartingLocation &&\n                        p.Name != pInfo.Name) != null)\n                    {\n                        SharedStartingLocationNotification();\n                        return;\n                    }\n                }\n\n                for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n                {\n                    int startingLocation = AIPlayers[aiId].StartingLocation;\n\n                    if (startingLocation == 0)\n                        continue;\n\n                    int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation);\n\n                    if (index > -1 && index != aiId)\n                    {\n                        SharedStartingLocationNotification();\n                        return;\n                    }\n                }\n\n                int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1)\n                    + AIPlayers.Count;\n\n                int minPlayers = GameModeMap.MinPlayers;\n                if (totalPlayerCount < minPlayers)\n                {\n                    InsufficientPlayersNotification();\n                    return;\n                }\n\n                if (GameModeMap.EnforceMaxPlayers && totalPlayerCount > GameModeMap.MaxPlayers)\n                {\n                    TooManyPlayersNotification();\n                    return;\n                }\n            }\n\n            int iId = 0;\n            foreach (PlayerInfo player in Players)\n            {\n                iId++;\n\n                if (player.Name == ProgramConstants.PLAYERNAME)\n                    continue;\n\n                if (!player.HashReceived)\n                {\n                    NotVerifiedNotification(iId - 1);\n                    return;\n                }\n\n\n                if (player.IsInGame)\n                {\n                    StillInGameNotification(iId - 1);\n                    return;\n                }\n                /*\n                if (DisableSpectatorReadyChecking)\n                {\n                    // Only account ready status if player is not a spectator\n                    if (!player.Ready && !IsPlayerSpectator(player))\n                    {\n                        GetReadyNotification();\n                        return;\n                    }\n                }\n                else\n                {\n                    if (!player.Ready)\n                    {\n                        GetReadyNotification();\n                        return;\n                    }\n                }\n                */\n\n                if (!player.Ready)\n                {\n                    GetReadyNotification();\n                    return;\n                }\n\n            }\n\n            HostLaunchGame();\n        }\n\n        protected virtual void LockGameNotification() =>\n            AddNotice(\"The host needs to lock the game room before launching the game.\".L10N(\"Client:Main:LockGameNotificationV2\"));\n\n        protected virtual void SharedColorsNotification() =>\n            AddNotice(\"Multiple human players cannot share the same color.\".L10N(\"Client:Main:SharedColorsNotification\"));\n\n        protected virtual void AISpectatorsNotification() =>\n            AddNotice(\"AI players don't enjoy spectating matches. They want some action!\".L10N(\"Client:Main:AISpectatorsNotification\"));\n\n        protected virtual void SharedStartingLocationNotification() =>\n            AddNotice(\"Multiple players cannot share the same starting location on this map.\".L10N(\"Client:Main:SharedStartingLocationNotification\"));\n\n        protected virtual void NotVerifiedNotification(int playerIndex)\n        {\n            if (playerIndex > -1 && playerIndex < Players.Count)\n                AddNotice(string.Format(\"Unable to launch game. Player {0} hasn't been verified.\".L10N(\"Client:Main:NotVerifiedNotification\"), Players[playerIndex].Name));\n        }\n\n        protected virtual void StillInGameNotification(int playerIndex)\n        {\n            if (playerIndex > -1 && playerIndex < Players.Count)\n            {\n                AddNotice(String.Format(\"Unable to launch game. Player {0} is still playing the game you started previously.\".L10N(\"Client:Main:StillInGameNotification\"),\n                    Players[playerIndex].Name));\n            }\n        }\n\n        protected virtual void GetReadyNotification()\n        {\n            AddNotice(\"The host wants to start the game but cannot because not all players are ready!\".L10N(\"Client:Main:GetReadyNotification\"));\n            if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready)\n                sndGetReadySound.Play();\n        }\n\n        protected virtual void InsufficientPlayersNotification()\n        {\n            Debug.Assert(GameModeMap != null, \"GameModeMap should not be null\");\n            AddNotice(string.Format(\"Unable to launch game: {0} cannot be played with fewer than {1} players\".L10N(\"Client:Main:InsufficientPlayersNotificationV2\"),\n                GameModeMap.ToString(), GameModeMap.MinPlayers));\n        }\n\n        protected virtual void TooManyPlayersNotification()\n        {\n            Debug.Assert(GameModeMap != null, \"GameModeMap should not be null\");\n            AddNotice(string.Format(\"Unable to launch game: {0} cannot be played with more than {1} players.\".L10N(\"Client:Main:TooManyPlayersNotificationV2\"),\n                GameModeMap.ToString(), GameModeMap.MaxPlayers));\n        }\n\n        public virtual void Clear()\n        {\n            if (!IsHost)\n                AIPlayers.Clear();\n\n            Players.Clear();\n        }\n\n        protected override void OnGameOptionChanged()\n        {\n            base.OnGameOptionChanged();\n\n            ClearReadyStatuses();\n            CopyPlayerDataToUI();\n        }\n\n        protected abstract void HostLaunchGame();\n\n        protected override void CopyPlayerDataFromUI(object sender, EventArgs e)\n        {\n            if (PlayerUpdatingInProgress)\n                return;\n\n            if (IsHost)\n            {\n                base.CopyPlayerDataFromUI(sender, e);\n                BroadcastPlayerOptions();\n                return;\n            }\n\n            int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME);\n\n            if (mTopIndex == -1)\n                return;\n\n            int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex;\n            int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex;\n            int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex;\n            int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex;\n\n            RequestPlayerOptions(requestedSide, requestedColor, requestedStart, requestedTeam);\n        }\n\n        protected override void CopyPlayerDataToUI()\n        {\n            if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT)\n                return;\n\n            base.CopyPlayerDataToUI();\n\n            ClearPingIndicators();\n\n            if (IsHost)\n            {\n                for (int pId = 1; pId < Players.Count; pId++)\n                    ddPlayerNames[pId].AllowDropDown = true;\n            }\n\n            // Player statuses\n            for (int pId = 0; pId < Players.Count; pId++)\n            {\n                /* if (pId != 0 && !Players[pId].HashReceived) // If player is not verified (not counting the host)\n                {\n                    StatusIndicators[pId].SwitchTexture(\"error\");\n                }\n                else */\n                if (Players[pId].IsInGame) // If player is ingame\n                {\n                    StatusIndicators[pId].SwitchTexture(PlayerSlotState.InGame);\n                }\n                else if (pId == 0) // If player is host\n                {\n                    StatusIndicators[pId].SwitchTexture(Locked ? PlayerSlotState.Ready : PlayerSlotState.NotReady); // Display room lock\n                }\n                else\n                {\n                    // StatusIndicators[pId].SwitchTexture(\n                    //     (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) \n                    //     ? \"okDisabled\" : \"ok\");\n                    StatusIndicators[pId].SwitchTexture(Players[pId].Ready ? PlayerSlotState.Ready : PlayerSlotState.NotReady);\n                }\n                /*\n                else\n                {\n                    // StatusIndicators[pId].SwitchTexture(\n                    //     (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) \n                    //     ? \"offDisabled\" : \"off\");\n\n                }\n                */\n\n                UpdatePlayerPingIndicator(Players[pId]);\n            }\n\n            // AI statuses\n            for (int aiId = 0; aiId < AIPlayers.Count; aiId++)\n            {\n                StatusIndicators[aiId + Players.Count].SwitchTexture(\n                    IsPlayerSpectator(AIPlayers[aiId]) ? PlayerSlotState.Error : PlayerSlotState.AI);\n\n                if (IsPlayerSpectator(AIPlayers[aiId]))\n                    StatusIndicators[aiId + Players.Count].ToolTip.Text += Environment.NewLine + \"AI players can't be spectators.\".L10N(\"Client:ClientGUI:AICantSpec\");\n            }\n\n            // Empty slot statuses\n            for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++)\n            {\n                StatusIndicators[i].SwitchTexture(PlayerSlotState.Empty);\n            }\n        }\n\n        protected virtual void ClearPingIndicators()\n        {\n            foreach (XNAClientDropDown dd in ddPlayerNames)\n            {\n                dd.Items[0].Texture = null;\n                dd.ToolTip.Text = string.Empty;\n            }\n        }\n\n        protected virtual void UpdatePlayerPingIndicator(PlayerInfo pInfo)\n        {\n            XNAClientDropDown ddPlayerName = ddPlayerNames[pInfo.Index];\n            ddPlayerName.Items[0].Texture = GetTextureForPing(pInfo.Ping);\n            if (pInfo.Ping < 0)\n                ddPlayerName.ToolTip.Text = \"Ping:\".L10N(\"Client:Main:PlayerInfoPing\") + \" ? \" + \"ms\".L10N(\"Client:Main:MillisecondsShort\");\n            else\n                ddPlayerName.ToolTip.Text = \"Ping:\".L10N(\"Client:Main:PlayerInfoPing\") + $\" {pInfo.Ping} \" + \"ms\".L10N(\"Client:Main:MillisecondsShort\");\n        }\n\n        private Texture2D GetTextureForPing(int ping)\n        {\n            switch (ping)\n            {\n                case int p when (p > 350):\n                    return PingTextures[4];\n                case int p when (p > 250):\n                    return PingTextures[3];\n                case int p when (p > 100):\n                    return PingTextures[2];\n                case int p when (p >= 0):\n                    return PingTextures[1];\n                default:\n                    return PingTextures[0];\n            }\n        }\n\n        protected abstract void BroadcastPlayerOptions();\n\n        protected abstract void BroadcastPlayerExtraOptions();\n\n        protected abstract void RequestPlayerOptions(int side, int color, int start, int team);\n\n        protected abstract void RequestReadyStatus();\n\n        // this public as it is used by the main lobby to notify the user of invitation failure\n        public void AddWarning(string message)\n        {\n            AddNotice(message, Color.Yellow);\n        }\n\n        protected override bool AllowPlayerOptionsChange() => IsHost;\n\n        protected override void ChangeMap(GameModeMap gameModeMap)\n        {\n            base.ChangeMap(gameModeMap);\n\n            bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap?.Map == null;\n\n            ClearReadyStatuses(resetAutoReady);\n\n            if ((LastMapChangeWasInvalid || resetAutoReady) && chkAutoReady.Checked)\n                RequestReadyStatus();\n\n            LastMapChangeWasInvalid = resetAutoReady;\n\n            //if (IsHost)\n            //    OnGameOptionChanged();\n        }\n\n        protected override void ToggleFavoriteMap()\n        {\n            base.ToggleFavoriteMap();\n\n            if ((GameModeMap != null && GameModeMap.IsFavorite) || !IsHost)\n                return;\n\n            RefreshForFavoriteMapRemoved();\n        }\n\n        protected override void WriteSpawnIniAdditions(IniFile iniFile)\n        {\n            base.WriteSpawnIniAdditions(iniFile);\n            iniFile.SetIntValue(\"Settings\", \"FrameSendRate\", FrameSendRate);\n            if (MaxAhead > 0)\n                iniFile.SetIntValue(\"Settings\", \"MaxAhead\", MaxAhead);\n            iniFile.SetIntValue(\"Settings\", \"Protocol\", ProtocolVersion);\n        }\n\n        protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap)\n        {\n            if (gameModeMap.MaxPlayers > 3)\n                return StatisticsManager.Instance.GetCoopRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers);\n\n            if (StatisticsManager.Instance.HasWonMapInPvP(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName, gameModeMap.MaxPlayers))\n                return 2;\n\n            return -1;\n        }\n\n        public void SwitchOn() => Enable();\n\n        public void SwitchOff() => Disable();\n\n        public abstract string GetSwitchName();\n\n        protected override void UpdateMapPreviewBoxEnabledStatus()\n        {\n            if (Map != null && GameMode != null)\n            {\n                bool disablestartlocs = GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts;\n                MapPreviewBox.EnableContextMenu = disablestartlocs ? false : IsHost;\n                MapPreviewBox.EnableStartLocationSelection = !disablestartlocs;\n            }\n            else\n            {\n                MapPreviewBox.EnableContextMenu = IsHost;\n                MapPreviewBox.EnableStartLocationSelection = true;\n            }\n        }\n\n        protected override bool UpdateLaunchGameButtonStatus()\n        {\n            if (IsHost)\n                btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && GameMode != null && Map != null;\n            else\n                btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !chkAutoReady.Checked;\n\n            return btnLaunchGame.Enabled;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs",
    "content": "﻿using Rampastring.XNAUI.XNAControls;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing ClientCore;\nusing Rampastring.XNAUI;\nusing Microsoft.Xna.Framework.Graphics;\nusing PlayerInfo = DTAClient.Domain.Multiplayer.PlayerInfo;\nusing Microsoft.Xna.Framework;\nusing DTAClient.Domain.Multiplayer;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    /// <summary>\n    /// A player location indicator for the map preview.\n    /// </summary>\n    public class PlayerLocationIndicator : XNAControl\n    {\n        const float TEXTURE_SCALE = 0.25f;\n\n        public PlayerLocationIndicator(WindowManager windowManager, List<MultiplayerColor> mpColors,\n            Color nameBackgroundColor, Color nameBorderColor, XNAContextMenu contextMenu) : base(windowManager)\n        {\n            this.mpColors = mpColors;\n            this.nameBackgroundColor = nameBackgroundColor;\n            this.nameBorderColor = nameBorderColor;\n            this.contextMenu = contextMenu;\n            HoverRemapColor = Color.White;\n            usePlayerRemapColor = ClientConfiguration.Instance.MapPreviewStartingLocationUsePlayerRemapColor;\n        }\n\n        private Texture2D baseTexture;\n        private Texture2D hoverTexture;\n        private Texture2D usedTexture;\n        public Texture2D WaypointTexture { get; set; }\n        public List<PlayerInfo> Players = new List<PlayerInfo>();\n\n        List<MultiplayerColor> mpColors;\n\n        public bool BackgroundShown { get; set; }\n\n        public int FontIndex { get; set; }\n\n        public double AngularVelocity = 0.015;\n        public double ReversedAngularVelocity = -0.0075;\n\n        public Color HoverRemapColor { get; set; }\n\n        private XNAContextMenu contextMenu { get; set; }\n\n        private Color nameBackgroundColor;\n        private Color nameBorderColor;\n\n        private readonly string[] teamIds = new[] { string.Empty }\n            .Concat(ProgramConstants.TEAMS.Select(team => $\"[{team}]\")).ToArray();\n\n        private bool usePlayerRemapColor = false;\n\n        private bool isHoveredOn = false;\n\n        private double backgroundAlpha = 0.0;\n        private double backgroundAlphaRate = 0.1;\n\n        private double angle;\n\n        private int lineHeight;\n\n        private Vector2 textSize;\n        private int textXPosition;\n\n        private List<PlayerText> pText = new List<PlayerText>();\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            baseTexture = AssetLoader.LoadTexture(\"slocindicator.png\");\n            hoverTexture = AssetLoader.LoadTexture(\"slocindicatorh.png\");\n            ClientRectangle = baseTexture.Bounds;\n            lineHeight = (int)Renderer.GetTextDimensions(\"@\", FontIndex).Y + 1;\n\n            usedTexture = baseTexture;\n        }\n\n        public void SetPosition(Point p)\n        {\n            int width = (int)(baseTexture.Width * TEXTURE_SCALE);\n            int height = (int)(baseTexture.Height * TEXTURE_SCALE);\n\n            ClientRectangle = new Rectangle(p.X - width / 2,\n                p.Y - height / 2,\n                width, height);\n        }\n\n        public void Refresh()\n        {\n            textSize = Vector2.Zero;\n            pText.Clear();\n\n            foreach (PlayerInfo pInfo in Players)\n            {\n                string text = pInfo.Name;\n                if (pInfo.TeamId > 0)\n                    text = teamIds[pInfo.TeamId] + \" \" + pInfo.Name;\n\n                if (text == null)\n                    return;\n\n                Vector2 pInfoSize = Renderer.GetTextDimensions(text, FontIndex);\n\n                if (pInfoSize.X > textSize.X)\n                    textSize = new Vector2(pInfoSize.X, Players.Count * (pInfoSize.Y + 1));\n\n                textXPosition = 3;\n\n                bool textOnRight = true;\n\n                if (Right + textXPosition + (int)textSize.X > Parent.Width)\n                {\n                    textXPosition = -(int)textSize.X - 3 - (int)(baseTexture.Width * TEXTURE_SCALE);\n                    text = pInfo.TeamId > 0 ? pInfo.Name + \" \" + teamIds[pInfo.TeamId] : pInfo.Name;\n                    textOnRight = false;\n                }\n\n                pText.Add(new PlayerText(text, textOnRight));\n            }\n        }\n\n        protected override void OnVisibleChanged(object sender, EventArgs args)\n        {\n            base.OnVisibleChanged(sender, args);\n\n            backgroundAlpha = 0.0;\n        }\n\n        public override void OnMouseEnter()\n        {\n            //usedTexture = hoverTexture;\n\n            isHoveredOn = true;\n\n            base.OnMouseEnter();\n        }\n\n        public override void OnMouseLeave()\n        {\n            //usedTexture = baseTexture;\n\n            isHoveredOn = false;\n\n            base.OnMouseLeave();\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            base.Update(gameTime);\n\n            double frameTimeCoefficient = gameTime.ElapsedGameTime.TotalMilliseconds / 10.0;\n\n            angle += Players.Count > 0 ? ReversedAngularVelocity * frameTimeCoefficient : AngularVelocity * frameTimeCoefficient;\n\n            if (Players.Count > 0)\n            {\n                usedTexture = hoverTexture;\n            }\n            else\n                usedTexture = baseTexture;\n\n            if (BackgroundShown)\n                backgroundAlpha = Math.Min(backgroundAlpha + backgroundAlphaRate, 1.0);\n            else\n                backgroundAlpha = Math.Max(backgroundAlpha - backgroundAlphaRate, 0.0);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            Point p = GetWindowPoint();\n            Rectangle displayRectangle = new Rectangle(p.X, p.Y, Width, Height);\n\n            int y = displayRectangle.Y + ((int)(baseTexture.Height * TEXTURE_SCALE) - lineHeight) / 2;\n\n            int i = 0;\n            foreach (PlayerInfo pInfo in Players)\n            {\n                Color textColor = Color.White;\n                if (pInfo.ColorId > 0)\n                    textColor = mpColors[pInfo.ColorId - 1].XnaColor;\n\n                if (backgroundAlpha > 0.0)\n                {\n                    int rectangleWidth = 0;\n                    int rectangleCoordX = 0;\n                    if (pText[i].TextOnRight)\n                    {\n                        rectangleCoordX = displayRectangle.Center.X;\n                        rectangleWidth = (int)textSize.X + textXPosition + displayRectangle.Width / 2 + 5;\n                    }\n                    else\n                    {\n                        rectangleWidth = (int)textSize.X + displayRectangle.Width / 2 + 5;\n                        rectangleCoordX = displayRectangle.Center.X - rectangleWidth;\n                    }\n\n                    Renderer.FillRectangle(new Rectangle(rectangleCoordX, y, rectangleWidth, lineHeight),\n                        new Color(nameBackgroundColor.R, nameBackgroundColor.G, nameBackgroundColor.B,\n                        (int)(nameBackgroundColor.A * backgroundAlpha)));\n\n                    Renderer.DrawRectangle(new Rectangle(rectangleCoordX, y, rectangleWidth, lineHeight),\n                        new Color(nameBorderColor.R, nameBorderColor.G, nameBorderColor.B, (int)(nameBorderColor.A * backgroundAlpha)));\n                }\n\n                Renderer.DrawStringWithShadow(pText[i].Text, FontIndex,\n                    new Vector2(displayRectangle.Right + textXPosition,\n                    y), textColor);\n\n                y += lineHeight;\n                i++;\n            }\n\n            Vector2 origin = new Vector2(usedTexture.Width / 2, usedTexture.Height / 2);\n\n            Renderer.DrawTexture(usedTexture,\n                new Vector2(displayRectangle.Center.X + 1.5f, displayRectangle.Center.Y + 1f),\n                (float)angle,\n                origin,\n                new Vector2(TEXTURE_SCALE), Color.Black);\n\n            Color remapColor = Color.White;\n            Color hoverRemapColor = HoverRemapColor;\n            if (Players.Count == 1 && Players[0].ColorId > 0)\n            {\n                remapColor = mpColors[Players[0].ColorId - 1].XnaColor;\n                hoverRemapColor = remapColor;\n            }\n\n            if (isHoveredOn ||\n                (contextMenu.Tag == this.Tag && contextMenu.Visible))\n            {\n                Renderer.DrawTexture(usedTexture,\n                new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y),\n                (float)angle,\n                origin,\n                new Vector2(TEXTURE_SCALE + 0.1f), hoverRemapColor);\n            }\n\n            Renderer.DrawTexture(usedTexture,\n                new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y),\n                (float)angle,\n                origin,\n                new Vector2(TEXTURE_SCALE), remapColor);\n\n            if (WaypointTexture != null)\n            {\n                // Non-premultiplied blending makes the indicators look sharper for some reason\n                // TODO figure out why\n                Renderer.PushSettings(new SpriteBatchSettings(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, null, null, null));\n\n                Renderer.DrawTexture(WaypointTexture,\n                    new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y),\n                    0f, \n                    new Vector2(WaypointTexture.Width / 2, WaypointTexture.Height / 2),\n                    new Vector2(1f, 1f),\n                    Color.White);\n\n                Renderer.PopSettings();\n            }\n\n            base.Draw(gameTime);\n        }\n\n        sealed class PlayerText\n        {\n            public PlayerText(string text, bool textOnRight)\n            {\n                Text = text;\n                TextOnRight = textOnRight;\n            }\n\n            public string Text { get; set; }\n            public bool TextOnRight { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Extensions;\nusing ClientCore.Statistics;\n\nusing ClientGUI;\n\nusing DTAClient.Domain;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.DXGUI.Generic;\n\nusing Microsoft.Xna.Framework;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.DXGUI.Multiplayer.GameLobby\n{\n    public class SkirmishLobby : GameLobbyBase, ISwitchable\n    {\n        private const string SETTINGS_PATH = \"Client/SkirmishSettings.ini\";\n\n        public SkirmishLobby(WindowManager windowManager, TopBar topBar, MapLoader mapLoader, DiscordHandler discordHandler, Random random)\n            : base(windowManager, \"SkirmishLobby\", mapLoader, false, discordHandler, random)\n        {\n            this.topBar = topBar;\n            this.random = random;\n        }\n\n        public event EventHandler Exited;\n\n        private Random random;\n\n        TopBar topBar;\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            RandomSeed = random.Next();\n\n            //InitPlayerOptionDropdowns(128, 98, 90, 48, 55, new Point(6, 24));\n            InitPlayerOptionDropdowns();\n\n            btnLeaveGame.Text = \"Main Menu\".L10N(\"Client:Main:MainMenu\");\n\n            //MapPreviewBox.EnableContextMenu = true;\n\n            const string spectatorName = \"Spectator\";\n            AddSideToDropDown(ddPlayerSides[0], spectatorName, spectatorName.L10N(\"Client:Sides:SpectatorSide\"), AssetLoader.LoadTexture(\"spectatoricon.png\"));\n\n            MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected;\n            MapPreviewBox.StartingLocationApplied += MapPreviewBox_StartingLocationApplied;\n\n            WindowManager.CenterControlOnScreen(this);\n\n            LoadSettings();\n\n            CopyPlayerDataToUI();\n\n            ProgramConstants.PlayerNameChanged += ProgramConstants_PlayerNameChanged;\n            ddPlayerSides[0].SelectedIndexChanged += PlayerSideChanged;\n\n            PlayerExtraOptionsPanel?.SetIsHost(true);\n        }\n\n        protected override void ToggleFavoriteMap()\n        {\n            base.ToggleFavoriteMap();\n\n            if (GameModeMap != null && GameModeMap.IsFavorite)\n                return;\n\n            RefreshForFavoriteMapRemoved();\n        }\n\n        protected override void AddNotice(string message, Color color)\n        {\n            XNAMessageBox.Show(WindowManager, \"Message\".L10N(\"Client:Main:MessageTitle\"), message);\n        }\n\n        protected override void OnEnabledChanged(object sender, EventArgs args)\n        {\n            base.OnEnabledChanged(sender, args);\n            if (Enabled)\n                UpdateDiscordPresence(true);\n            else\n                ResetDiscordPresence();\n        }\n\n        private void ProgramConstants_PlayerNameChanged(object sender, EventArgs e)\n        {\n            Players[0].Name = ProgramConstants.PLAYERNAME;\n            CopyPlayerDataToUI();\n        }\n\n        private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e)\n        {\n            CopyPlayerDataToUI();\n        }\n\n        private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalStartingLocationEventArgs e)\n        {\n            Players[0].StartingLocation = e.StartingLocationIndex + 1;\n            CopyPlayerDataToUI();\n        }\n\n        private string CheckGameValidity()\n        {\n            int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1)\n                + AIPlayers.Count;\n\n            if (GameModeMap.MultiplayerOnly)\n            {\n                return string.Format(\"{0} can only be played on CnCNet and LAN.\".L10N(\"Client:Main:GameModeMultiplayerOnly\"),\n                    GameModeMap.ToString());\n            }\n\n            if (totalPlayerCount < GameModeMap.MinPlayers)\n            {\n                return string.Format(\"{0} cannot be played with less than {1} players.\".L10N(\"Client:Main:GameModeInsufficientPlayers\"),\n                    GameModeMap.ToString(), GameModeMap.MinPlayers);\n            }\n\n            if (GameModeMap.EnforceMaxPlayers)\n            {\n                if (totalPlayerCount > GameModeMap.MaxPlayers)\n                {\n                    return string.Format(\"{0} cannot be played with more than {1} players.\".L10N(\"Client:Main:TooManyPlayers\"),\n                        GameModeMap.ToString(), GameModeMap.MaxPlayers);\n                }\n\n                IEnumerable<PlayerInfo> concatList = Players.Concat(AIPlayers);\n\n                foreach (PlayerInfo pInfo in concatList)\n                {\n                    if (pInfo.StartingLocation == 0)\n                        continue;\n\n                    if (concatList.Count(p => p.StartingLocation == pInfo.StartingLocation) > 1)\n                    {\n                        return \"Multiple players cannot share the same starting location on the selected map.\".L10N(\"Client:Main:StartLocationOccupied\");\n                    }\n                }\n            }\n\n            if (GameModeMap.IsCoop && Players[0].SideId == ddPlayerSides[0].Items.Count - 1)\n            {\n                return \"Co-op missions cannot be spectated. You'll have to show a bit more effort to cheat here.\".L10N(\"Client:Main:CoOpMissionSpectatorPrompt\");\n            }\n\n            var teamMappingsError = GetTeamMappingsError();\n            if (!string.IsNullOrEmpty(teamMappingsError))\n                return teamMappingsError;\n\n            return null;\n        }\n\n        protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e)\n        {\n            string error = CheckGameValidity();\n\n            if (error == null)\n            {\n                SaveSettings();\n                StartGame();\n                return;\n            }\n\n            XNAMessageBox.Show(WindowManager, \"Cannot launch game\".L10N(\"Client:Main:LaunchGameErrorTitle\"), error);\n        }\n\n        protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e)\n        {\n            Exited?.Invoke(this, EventArgs.Empty);\n\n            PlayerExtraOptionsPanel?.Disable();\n            Disable();\n\n            topBar.RemovePrimarySwitchable(this);\n            ResetDiscordPresence();\n        }\n\n        private void PlayerSideChanged(object sender, EventArgs e)\n        {\n            UpdateDiscordPresence();\n        }\n\n        protected override void UpdateDiscordPresence(bool resetTimer = false)\n        {\n            if (discordHandler == null || Map == null || GameMode == null || !Initialized)\n                return;\n\n            int playerIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME);\n            if (playerIndex >= MAX_PLAYER_COUNT || playerIndex < 0)\n                return;\n\n            XNAClientDropDown sideDropDown = ddPlayerSides[playerIndex];\n            if (sideDropDown.SelectedItem == null)\n                return;\n\n            string side = (string)sideDropDown.SelectedItem.Tag;\n            string currentState = ProgramConstants.IsInGame ? \"In Game\" : \"Setting Up\";\n\n            discordHandler.UpdatePresence(\n                Map.UntranslatedName, GameMode.UntranslatedUIName, currentState, side, resetTimer);\n        }\n\n        protected override bool AllowPlayerOptionsChange()\n        {\n            return true;\n        }\n\n        protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap)\n        {\n            return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers);\n        }\n\n        protected override void GameProcessExited()\n        {\n            base.GameProcessExited();\n\n            DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks\n\n            RandomSeed = random.Next();\n        }\n\n        public void Open()\n        {\n            topBar.AddPrimarySwitchable(this);\n            Enable();\n        }\n\n        public void SwitchOn()\n        {\n            Enable();\n        }\n\n        public void SwitchOff()\n        {\n            Disable();\n        }\n\n        public string GetSwitchName()\n        {\n            return \"Skirmish Lobby\".L10N(\"Client:Main:SkirmishLobby\");\n        }\n\n        /// <summary>\n        /// Saves skirmish settings to an INI file on the file system.\n        /// </summary>\n        private void SaveSettings()\n        {\n            try\n            {\n                FileInfo settingsFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH);\n\n                // Delete the file so we don't keep potential extra AI players that already exist in the file\n                settingsFileInfo.Delete();\n\n                var skirmishSettingsIni = new IniFile(settingsFileInfo.FullName);\n\n                skirmishSettingsIni.SetStringValue(\"Player\", \"Info\", Players[0].ToString());\n\n                for (int i = 0; i < AIPlayers.Count; i++)\n                {\n                    skirmishSettingsIni.SetStringValue(\"AIPlayers\", i.ToString(), AIPlayers[i].ToString());\n                }\n\n                skirmishSettingsIni.SetStringValue(\"Settings\", \"Map\", Map?.SHA1 ?? string.Empty);\n                skirmishSettingsIni.SetStringValue(\"Settings\", \"GameModeMapFilter\", ddGameModeMapFilter.SelectedItem?.Text);\n\n                if (ClientConfiguration.Instance.SaveSkirmishGameOptions)\n                {\n                    foreach (GameLobbyDropDown dd in DropDowns)\n                    {\n                        skirmishSettingsIni.SetStringValue(\"GameOptions\", dd.Name, dd.UserSelectedIndex + \"\");\n                    }\n\n                    foreach (GameLobbyCheckBox cb in CheckBoxes)\n                    {\n                        skirmishSettingsIni.SetStringValue(\"GameOptions\", cb.Name, cb.Checked.ToString());\n                    }\n                }\n\n                skirmishSettingsIni.WriteIniFile();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Saving skirmish settings failed! Reason: \" + ex.ToString());\n\n#if DEBUG\n                Debugger.Break();\n#endif\n            }\n        }\n\n        /// <summary>\n        /// Loads skirmish settings from an INI file on the file system.\n        /// </summary>\n        private void LoadSettings()\n        {\n            if (!SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH).Exists)\n            {\n                InitDefaultSettings();\n                return;\n            }\n\n            var skirmishSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, SETTINGS_PATH));\n\n            string gameModeMapFilterName = skirmishSettingsIni.GetStringValue(\"Settings\", \"GameModeMapFilter\", string.Empty);\n            if (string.IsNullOrEmpty(gameModeMapFilterName))\n                gameModeMapFilterName = skirmishSettingsIni.GetStringValue(\"Settings\", \"GameMode\", string.Empty); // legacy\n\n            var gameModeMapFilter = ddGameModeMapFilter.Items.Find(i => i.Text == gameModeMapFilterName)?.Tag as GameModeMapFilter;\n            if (gameModeMapFilter == null || !gameModeMapFilter.Any())\n                gameModeMapFilter = GetDefaultGameModeMapFilter();\n\n            var gameModeMap = gameModeMapFilter.GetGameModeMaps().FirstOrDefault();\n\n            if (gameModeMap != null)\n            {\n                GameModeMap = gameModeMap;\n\n                ddGameModeMapFilter.SelectedIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Tag == gameModeMapFilter);\n\n                string mapSHA1 = skirmishSettingsIni.GetStringValue(\"Settings\", \"Map\", string.Empty);\n\n                int gameModeMapIndex = GetSortedGameModeMaps().FindIndex(gmm => gmm.Map.SHA1 == mapSHA1);\n\n                if (gameModeMapIndex > -1)\n                {\n                    lbGameModeMapList.SelectedIndex = gameModeMapIndex;\n\n                    while (gameModeMapIndex > lbGameModeMapList.LastIndex)\n                        lbGameModeMapList.TopIndex++;\n                }\n            }\n            else\n                LoadDefaultGameModeMap();\n\n            var player = PlayerInfo.FromString(skirmishSettingsIni.GetStringValue(\"Player\", \"Info\", string.Empty));\n\n            if (player == null)\n            {\n                Logger.Log(\"Failed to load human player information from skirmish settings!\");\n                InitDefaultSettings();\n                return;\n            }\n\n            CheckLoadedPlayerVariableBounds(player);\n\n            player.Name = ProgramConstants.PLAYERNAME;\n            Players.Add(player);\n\n            List<string> keys = skirmishSettingsIni.GetSectionKeys(\"AIPlayers\");\n\n            if (keys == null)\n            {\n                keys = new List<string>(); // No point skip parsing all settings if only AI info is missing.\n                //Logger.Log(\"AI player information doesn't exist in skirmish settings!\");\n                //InitDefaultSettings();\n                //return;\n            }\n\n            bool AIAllowed = GameModeMap != null && !GameModeMap.HumanPlayersOnly;\n            foreach (string key in keys)\n            {\n                if (!AIAllowed) break;\n                var aiPlayer = PlayerInfo.FromString(skirmishSettingsIni.GetStringValue(\"AIPlayers\", key, string.Empty));\n\n                CheckLoadedPlayerVariableBounds(aiPlayer, true);\n\n                if (aiPlayer == null)\n                {\n                    Logger.Log(\"Failed to load AI player information from skirmish settings!\");\n                    InitDefaultSettings();\n                    return;\n                }\n\n                if (AIPlayers.Count < MAX_PLAYER_COUNT - 1)\n                    AIPlayers.Add(aiPlayer);\n            }\n\n            if (ClientConfiguration.Instance.SaveSkirmishGameOptions)\n            {\n                foreach (GameLobbyDropDown dd in DropDowns)\n                {\n                    // Maybe we should build an union of the game mode and map\n                    // forced options, we'd have less repetitive code that way\n\n                    if (GameMode != null)\n                    {\n                        int gameModeMatchIndex = GameMode.ForcedDropDownValues.FindIndex(p => p.Key.Equals(dd.Name));\n                        if (gameModeMatchIndex > -1)\n                        {\n                            Logger.Log(\"Dropdown '\" + dd.Name + \"' has forced value in gamemode - saved settings ignored.\");\n                            continue;\n                        }\n                    }\n\n                    if (Map != null)\n                    {\n                        int gameModeMatchIndex = Map.ForcedDropDownValues.FindIndex(p => p.Key.Equals(dd.Name));\n                        if (gameModeMatchIndex > -1)\n                        {\n                            Logger.Log(\"Dropdown '\" + dd.Name + \"' has forced value in map - saved settings ignored.\");\n                            continue;\n                        }\n                    }\n\n                    dd.UserSelectedIndex = skirmishSettingsIni.GetIntValue(\"GameOptions\", dd.Name, dd.UserSelectedIndex);\n\n                    if (dd.UserSelectedIndex > -1 && dd.UserSelectedIndex < dd.Items.Count)\n                        dd.SelectedIndex = dd.UserSelectedIndex;\n                }\n\n                foreach (GameLobbyCheckBox cb in CheckBoxes)\n                {\n                    if (GameMode != null)\n                    {\n                        int gameModeMatchIndex = GameMode.ForcedCheckBoxValues.FindIndex(p => p.Key.Equals(cb.Name));\n                        if (gameModeMatchIndex > -1)\n                        {\n                            Logger.Log(\"Checkbox '\" + cb.Name + \"' has forced value in gamemode - saved settings ignored.\");\n                            continue;\n                        }\n                    }\n\n                    if (Map != null)\n                    {\n                        int gameModeMatchIndex = Map.ForcedCheckBoxValues.FindIndex(p => p.Key.Equals(cb.Name));\n                        if (gameModeMatchIndex > -1)\n                        {\n                            Logger.Log(\"Checkbox '\" + cb.Name + \"' has forced value in map - saved settings ignored.\");\n                            continue;\n                        }\n                    }\n\n                    cb.Checked = skirmishSettingsIni.GetBooleanValue(\"GameOptions\", cb.Name, cb.Checked);\n                }\n            }\n        }\n\n        /// <summary>\n        /// Checks that a player's color, team and starting location\n        /// don't exceed allowed bounds.\n        /// </summary>\n        /// <param name=\"pInfo\">The PlayerInfo.</param>\n        private void CheckLoadedPlayerVariableBounds(PlayerInfo pInfo, bool isAIPlayer = false)\n        {\n            int sideCount = SideCount + RandomSelectorCount;\n            if (isAIPlayer) sideCount--;\n\n            if (pInfo.SideId < 0 || pInfo.SideId > sideCount)\n            {\n                pInfo.SideId = 0;\n            }\n\n            if (pInfo.ColorId < 0 || pInfo.ColorId > MPColors.Count)\n            {\n                pInfo.ColorId = 0;\n            }\n\n            if (pInfo.TeamId < 0 || pInfo.TeamId >= ddPlayerTeams[0].Items.Count ||\n                (!(GameModeMap?.IsCoop ?? false)) && (GameModeMap?.ForceNoTeams ?? false))\n            {\n                pInfo.TeamId = 0;\n            }\n\n            if (pInfo.StartingLocation < 0 || pInfo.StartingLocation > MAX_PLAYER_COUNT ||\n                (GameModeMap?.ForceRandomStartLocations ?? false))\n            {\n                pInfo.StartingLocation = 0;\n            }\n        }\n\n        private void InitDefaultSettings()\n        {\n            Players.Clear();\n            AIPlayers.Clear();\n\n            Players.Add(new PlayerInfo(ProgramConstants.PLAYERNAME, 0, 0, 0, 0));\n            PlayerInfo aiPlayer = new PlayerInfo(ProgramConstants.AI_PLAYER_NAMES[0], 0, 0, 0, 0);\n            aiPlayer.IsAI = true;\n            aiPlayer.AILevel = 0;\n            AIPlayers.Add(aiPlayer);\n\n            LoadDefaultGameModeMap();\n        }\n\n        protected override void UpdateMapPreviewBoxEnabledStatus()\n        {\n            MapPreviewBox.EnableContextMenu = GameModeMap != null && !(GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts);\n            MapPreviewBox.EnableStartLocationSelection = MapPreviewBox.EnableContextMenu;\n        }\n\n        protected override bool UpdateLaunchGameButtonStatus()\n        {\n            btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && GameMode != null && Map != null;\n            return btnLaunchGame.Enabled;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANGameCreationWindow.cs",
    "content": "﻿using ClientGUI;\nusing System;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing Microsoft.Xna.Framework;\nusing ClientCore;\nusing System.IO;\nusing Rampastring.Tools;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// A window that makes it possible for a LAN player who's hosting a game\n    /// to pick between hosting a new game and hosting a loaded game.\n    /// </summary>\n    class LANGameCreationWindow : XNAWindow\n    {\n        public LANGameCreationWindow(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public event EventHandler NewGame;\n        public event EventHandler<GameLoadEventArgs> LoadGame;\n\n        private XNALabel lblDescription;\n\n        private XNAButton btnNewGame;\n        private XNAButton btnLoadGame;\n        private XNAButton btnCancel;\n\n        public override void Initialize()\n        {\n            Name = \"LANGameCreationWindow\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"gamecreationoptionsbg.png\");\n            ClientRectangle = new Rectangle(0, 0, 447, 77);\n\n            lblDescription = new XNALabel(WindowManager);\n            lblDescription.Name = \"lblDescription\";\n            lblDescription.FontIndex = 1;\n            lblDescription.Text = \"SELECT SESSION TYPE\".L10N(\"Client:Main:SelectMissionType\");\n\n            AddChild(lblDescription);\n\n            lblDescription.CenterOnParent();\n            lblDescription.ClientRectangle = new Rectangle(\n                lblDescription.X,\n                12,\n                lblDescription.Width,\n                lblDescription.Height);\n\n            btnNewGame = new XNAButton(WindowManager);\n            btnNewGame.Name = \"btnNewGame\";\n            btnNewGame.ClientRectangle = new Rectangle(12, 42, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnNewGame.IdleTexture = AssetLoader.LoadTexture(\"133pxbtn.png\");\n            btnNewGame.HoverTexture = AssetLoader.LoadTexture(\"133pxbtn_c.png\");\n            btnNewGame.FontIndex = 1;\n            btnNewGame.Text = \"New Game\".L10N(\"Client:Main:NewGame\");\n            btnNewGame.HoverSoundEffect = new EnhancedSoundEffect(\"button.wav\");\n            btnNewGame.LeftClick += BtnNewGame_LeftClick;\n\n            btnLoadGame = new XNAButton(WindowManager);\n            btnLoadGame.Name = \"btnLoadGame\";\n            btnLoadGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12,\n                btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnLoadGame.IdleTexture = btnNewGame.IdleTexture;\n            btnLoadGame.HoverTexture = btnNewGame.HoverTexture;\n            btnLoadGame.FontIndex = 1;\n            btnLoadGame.Text = \"Load Game\".L10N(\"Client:Main:LoadGame\");\n            btnLoadGame.HoverSoundEffect = btnNewGame.HoverSoundEffect;\n            btnLoadGame.LeftClick += BtnLoadGame_LeftClick;\n\n            btnCancel = new XNAButton(WindowManager);\n            btnCancel.Name = \"btnCancel\";\n            btnCancel.ClientRectangle = new Rectangle(btnLoadGame.Right + 12,\n                btnNewGame.Y, 133, 23);\n            btnCancel.IdleTexture = btnNewGame.IdleTexture;\n            btnCancel.HoverTexture = btnNewGame.HoverTexture;\n            btnCancel.FontIndex = 1;\n            btnCancel.Text = \"Cancel\".L10N(\"Client:Main:ButtonCancel\");\n            btnCancel.HoverSoundEffect = btnNewGame.HoverSoundEffect;\n            btnCancel.LeftClick += BtnCancel_LeftClick;\n\n            AddChild(btnNewGame);\n            AddChild(btnLoadGame);\n            AddChild(btnCancel);\n\n            base.Initialize();\n\n            CenterOnParent();\n        }\n\n        private void BtnNewGame_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n            NewGame?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void BtnLoadGame_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n\n            IniFile iniFile = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI));\n\n            LoadGame?.Invoke(this, new GameLoadEventArgs(iniFile.GetIntValue(\"Settings\", \"GameID\", -1)));\n        }\n\n        private void BtnCancel_LeftClick(object sender, EventArgs e)\n        {\n            Disable();\n        }\n\n        public void Open()\n        {\n            btnLoadGame.AllowClick = AllowLoadingGame();\n            Enable();\n        }\n\n        private bool AllowLoadingGame()\n        {\n            FileInfo savedGameSpawnIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI);\n\n            if (!savedGameSpawnIniFile.Exists)\n                return false;\n\n            IniFile iniFile = new IniFile(savedGameSpawnIniFile.FullName);\n            if (iniFile.GetStringValue(\"Settings\", \"Name\", string.Empty) != ProgramConstants.PLAYERNAME)\n                return false;\n\n            if (!iniFile.GetBooleanValue(\"Settings\", \"Host\", false))\n                return false;\n\n            // Don't allow loading CnCNet games in LAN mode\n            if (iniFile.SectionExists(\"Tunnel\"))\n                return false;\n\n            return true;\n        }\n    }\n\n    public class GameLoadEventArgs : EventArgs\n    {\n        public GameLoadEventArgs(int loadedGameId)\n        {\n            LoadedGameID = loadedGameId;\n        }\n\n        public int LoadedGameID { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Domain;\nusing DTAClient.Domain.LAN;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.LAN;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing DTAClient.Online;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    class LANGameLoadingLobby : GameLoadingLobbyBase\n    {\n        private const double DROPOUT_TIMEOUT = 20.0;\n        private const double GAME_BROADCAST_INTERVAL = 2.0;\n\n        private const string OPTIONS_COMMAND = \"OPTS\";\n        private const string GAME_LAUNCH_COMMAND = \"START\";\n        private const string READY_STATUS_COMMAND = \"READY\";\n        private const string CHAT_COMMAND = \"CHAT\";\n        private const string PLAYER_QUIT_COMMAND = \"QUIT\";\n        private const string PLAYER_JOIN_COMMAND = \"JOIN\";\n        private const string FILE_HASH_COMMAND = \"FHASH\";\n\n        public LANGameLoadingLobby(\n            WindowManager windowManager,\n            LANColor[] chatColors,\n            MapLoader mapLoader,\n            DiscordHandler discordHandler\n            ) : base(windowManager, discordHandler)\n        {\n            encoding = ProgramConstants.LAN_ENCODING;\n            this.chatColors = chatColors;\n            this.mapLoader = mapLoader;\n\n            localGame = ClientConfiguration.Instance.LocalGame;\n\n            hostCommandHandlers = new LANServerCommandHandler[]\n            {\n                new ServerStringCommandHandler(CHAT_COMMAND, Server_HandleChatMessage),\n                new ServerStringCommandHandler(FILE_HASH_COMMAND, Server_HandleFileHashMessage),\n                new ServerNoParamCommandHandler(READY_STATUS_COMMAND, Server_HandleReadyRequest),\n            };\n\n            playerCommandHandlers = new LANClientCommandHandler[]\n            {\n                new ClientStringCommandHandler(CHAT_COMMAND, Client_HandleChatMessage),\n                new ClientStringCommandHandler(OPTIONS_COMMAND, Client_HandleOptionsMessage),\n                new ClientNoParamCommandHandler(GAME_LAUNCH_COMMAND, Client_HandleStartCommand),\n                new ClientNoParamCommandHandler(PLAYER_QUIT_COMMAND, HandleHostQuit),\n            };\n\n            WindowManager.GameClosing += WindowManager_GameClosing;\n        }\n\n        private void WindowManager_GameClosing(object sender, EventArgs e)\n        {\n            if (client != null && client.Connected)\n                Clear();\n        }\n\n        public event EventHandler<LobbyNotificationEventArgs> LobbyNotification;\n        public event EventHandler<GameBroadcastEventArgs> GameBroadcast;\n\n        private TcpListener listener;\n        private TcpClient client;\n\n        private IPEndPoint hostEndPoint;\n        private LANColor[] chatColors;\n        private readonly MapLoader mapLoader;\n        private int chatColorIndex;\n        private Encoding encoding;\n\n        private LANServerCommandHandler[] hostCommandHandlers;\n        private LANClientCommandHandler[] playerCommandHandlers;\n\n        private TimeSpan timeSinceGameBroadcast = TimeSpan.Zero;\n\n        private TimeSpan timeSinceLastReceivedCommand = TimeSpan.Zero;\n\n        private string overMessage = string.Empty;\n\n        private string localGame;\n\n        private string localFileHash;\n\n        private IReadOnlyList<GameMode> gameModes => mapLoader.GameModes;\n\n        private int loadedGameId;\n\n        private bool started = false;\n        private volatile bool leaving;\n        private int sessionId;\n\n        public void SetUp(bool isHost,\n            IPEndPoint hostEndPoint, TcpClient client,\n            int loadedGameId)\n        {\n            leaving = false;\n            sessionId++;\n            Refresh(isHost);\n\n            this.hostEndPoint = hostEndPoint;\n\n            this.loadedGameId = loadedGameId;\n\n            started = false;\n\n            if (isHost)\n            {\n                Thread thread = new Thread(ListenForClients);\n                thread.Start();\n\n                this.client = new TcpClient();\n                this.client.Connect(\"127.0.0.1\", ProgramConstants.LAN_GAME_LOBBY_PORT);\n\n                byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND +\n                    ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME +\n                    ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId);\n\n                this.client.GetStream().Write(buffer, 0, buffer.Length);\n                this.client.GetStream().Flush();\n\n                var fhc = new FileHashCalculator();\n                fhc.CalculateHashes();\n                localFileHash = fhc.GetCompleteHash();\n            }\n            else\n            {\n                this.client = client;\n            }\n\n            new Thread(HandleServerCommunication).Start();\n\n            if (IsHost)\n                CopyPlayerDataToUI();\n\n            WindowManager.SelectedControl = tbChatInput;\n        }\n\n        public void PostJoin()\n        {\n            var fhc = new FileHashCalculator();\n            fhc.CalculateHashes();\n            SendMessageToHost(FILE_HASH_COMMAND + \" \" + fhc.GetCompleteHash());\n            UpdateDiscordPresence(true);\n        }\n\n        #region Server code\n\n        private void ListenForClients()\n        {\n            listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT);\n            listener.Start();\n\n            while (true)\n            {\n                TcpClient client;\n\n                try\n                {\n                    client = listener.AcceptTcpClient();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Listener error: \" + ex.ToString());\n                    break;\n                }\n\n                Logger.Log(\"New client connected from \" + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString());\n\n                LANPlayerInfo lpInfo = new LANPlayerInfo(encoding);\n                lpInfo.SetClient(client);\n\n                Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection));\n                thread.Start(lpInfo);\n            }\n        }\n\n        private void HandleClientConnection(object clientInfo)\n        {\n            var lpInfo = (LANPlayerInfo)clientInfo;\n\n            byte[] message = new byte[1024];\n\n            while (true)\n            {\n                int bytesRead = 0;\n\n                try\n                {\n                    bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Socket error with client \" + lpInfo.IPAddress + \"; removing. Message: \" + ex.ToString());\n                    break;\n                }\n\n                if (bytesRead == 0)\n                {\n                    Logger.Log(\"Connect attempt from \" + lpInfo.IPAddress + \" failed! (0 bytes read)\");\n\n                    break;\n                }\n\n                string msg = encoding.GetString(message, 0, bytesRead);\n\n                string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR);\n                string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n                if (parts.Length != 3)\n                    break;\n\n                string name = parts[1].Trim();\n                int loadedGameId = Conversions.IntFromString(parts[2], -1);\n\n                if (parts[0] == \"JOIN\" && !string.IsNullOrEmpty(name)\n                    && loadedGameId == this.loadedGameId)\n                {\n                    lpInfo.Name = name;\n\n                    AddCallback(new Action<LANPlayerInfo>(AddPlayer), lpInfo);\n                    return;\n                }\n\n                break;\n            }\n\n            if (lpInfo.TcpClient.Connected)\n                lpInfo.TcpClient.Close();\n        }\n\n        private void AddPlayer(LANPlayerInfo lpInfo)\n        {\n            if (Players.Find(p => p.Name == lpInfo.Name) != null ||\n                Players.Count >= SGPlayers.Count ||\n                SGPlayers.Find(p => p.Name == lpInfo.Name) == null)\n            {\n                lpInfo.TcpClient.Close();\n                return;\n            }\n\n            if (Players.Count == 0)\n                lpInfo.Ready = true;\n\n            Players.Add(lpInfo);\n\n            lpInfo.MessageReceived += LpInfo_MessageReceived;\n            lpInfo.ConnectionLost += LpInfo_ConnectionLost;\n\n            sndJoinSound.Play();\n\n            AddNotice(string.Format(\"{0} connected from {1}\".L10N(\"Client:Main:PlayerFromIP\"), lpInfo.Name, lpInfo.IPAddress));\n            lpInfo.StartReceiveLoop();\n\n            CopyPlayerDataToUI();\n            BroadcastOptions();\n            UpdateDiscordPresence();\n        }\n\n        private void LpInfo_ConnectionLost(object sender, EventArgs e)\n        {\n            AddCallback(new Action<LANPlayerInfo>(HandleConnectionLost), (LANPlayerInfo)sender);\n        }\n\n        private void HandleConnectionLost(LANPlayerInfo lpInfo)\n        {\n            CleanUpPlayer(lpInfo);\n            Players.Remove(lpInfo);\n\n            AddNotice(string.Format(\"{0} has left the game.\".L10N(\"Client:Main:PlayerLeftGame\"), lpInfo.Name));\n\n            sndLeaveSound.Play();\n\n            CopyPlayerDataToUI();\n            BroadcastOptions();\n            UpdateDiscordPresence();\n        }\n\n        private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e)\n        {\n            AddCallback(new Action<string, LANPlayerInfo>(HandleClientMessage),\n                e.Message, (LANPlayerInfo)sender);\n        }\n\n        private void HandleClientMessage(string data, LANPlayerInfo lpInfo)\n        {\n            lpInfo.TimeSinceLastReceivedMessage = TimeSpan.Zero;\n\n            foreach (var cmdHandler in hostCommandHandlers)\n            {\n                if (cmdHandler.Handle(lpInfo, data))\n                    return;\n            }\n\n            Logger.Log(\"Unknown LAN command from \" + lpInfo.ToString() + \" : \" + data);\n        }\n\n        private void CleanUpPlayer(LANPlayerInfo lpInfo)\n        {\n            lpInfo.MessageReceived -= LpInfo_MessageReceived;\n            lpInfo.ConnectionLost -= LpInfo_ConnectionLost;\n            lpInfo.TcpClient.Close();\n        }\n\n        #endregion\n\n        private void HandleServerCommunication()\n        {\n            byte[] message = new byte[1024];\n\n            var msg = string.Empty;\n\n            int bytesRead = 0;\n\n            int mySessionId = sessionId;\n\n            if (!client.Connected)\n                return;\n\n            var stream = client.GetStream();\n\n            while (true)\n            {\n                bytesRead = 0;\n\n                try\n                {\n                    bytesRead = stream.Read(message, 0, message.Length);\n                }\n                catch (Exception ex)\n                {\n                    if (leaving)\n                        break;\n\n                    Logger.Log(\"Reading data from the server failed! Message: \" + ex.ToString());\n                    AddCallback(() =>\n                    {\n                        if (sessionId == mySessionId)\n                            LeaveGame();\n                    });\n                    break;\n                }\n\n                if (bytesRead > 0)\n                {\n                    msg = encoding.GetString(message, 0, bytesRead);\n\n                    msg = overMessage + msg;\n                    List<string> commands = new List<string>();\n\n                    while (true)\n                    {\n                        int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n                        if (index == -1)\n                        {\n                            overMessage = msg;\n                            break;\n                        }\n                        else\n                        {\n                            commands.Add(msg.Substring(0, index));\n                            msg = msg.Substring(index + 1);\n                        }\n                    }\n\n                    foreach (string cmd in commands)\n                    {\n                        string capturedCmd = cmd;\n                        AddCallback(() =>\n                        {\n                            if (sessionId == mySessionId)\n                                HandleMessageFromServer(capturedCmd);\n                        });\n                    }\n\n                    continue;\n                }\n\n                if (leaving)\n                    break;\n\n                Logger.Log(\"Reading data from the server failed (0 bytes received)!\");\n                AddCallback(() => { if (sessionId == mySessionId) LeaveGame(); });\n                break;\n            }\n        }\n\n        private void HandleMessageFromServer(string message)\n        {\n            timeSinceLastReceivedCommand = TimeSpan.Zero;\n\n            foreach (var cmdHandler in playerCommandHandlers)\n            {\n                if (cmdHandler.Handle(message))\n                    return;\n            }\n\n            Logger.Log(\"Unknown LAN command from the server: \" + message);\n        }\n\n        private void HandleHostQuit()\n        {\n            if (!IsHost && !leaving)\n                LeaveGame();\n        }\n\n        protected override void LeaveGame()\n        {\n            if (leaving)\n                return;\n\n            Clear();\n            Disable();\n\n            base.LeaveGame();\n        }\n\n        private void Clear()\n        {\n            if (IsHost)\n            {\n                GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(\"GAMECLOSED\"));\n                BroadcastMessage(PLAYER_QUIT_COMMAND);\n                Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p));\n                Players.Clear();\n                listener.Stop();\n            }\n            else\n            {\n                SendMessageToHost(PLAYER_QUIT_COMMAND);\n            }\n\n            leaving = true;\n\n            if (this.client.Connected)\n                this.client.Close();\n        }\n\n        protected override void AddNotice(string message, Color color)\n        {\n            lbChatMessages.AddMessage(null, message, color);\n        }\n\n        protected override void BroadcastOptions()\n        {\n            if (Players.Count > 0)\n                Players[0].Ready = true;\n\n            var sb = new ExtendedStringBuilder(OPTIONS_COMMAND + \" \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n\n            sb.Append(ddSavedGame.SelectedIndex);\n\n            foreach (PlayerInfo pInfo in Players)\n            {\n                sb.Append(pInfo.Name);\n                sb.Append(Convert.ToInt32(pInfo.Ready));\n                sb.Append(pInfo.IPAddress);\n            }\n\n            BroadcastMessage(sb.ToString());\n        }\n\n        protected override void HostStartGame()\n        {\n            BroadcastMessage(GAME_LAUNCH_COMMAND);\n        }\n\n        protected override void RequestReadyStatus()\n        {\n            SendMessageToHost(READY_STATUS_COMMAND);\n        }\n\n        protected override void SendChatMessage(string message)\n        {\n            SendMessageToHost(CHAT_COMMAND + \" \" + chatColorIndex +\n                ProgramConstants.LAN_DATA_SEPARATOR + message);\n\n            sndMessageSound.Play();\n        }\n\n        #region Server's command handlers\n\n        private void Server_HandleChatMessage(LANPlayerInfo sender, string data)\n        {\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length < 2)\n                return;\n\n            int colorIndex = Conversions.IntFromString(parts[0], -1);\n\n            if (colorIndex < 0 || colorIndex >= chatColors.Length)\n                return;\n\n            BroadcastMessage(CHAT_COMMAND + \" \" + sender +\n                ProgramConstants.LAN_DATA_SEPARATOR + colorIndex +\n                ProgramConstants.LAN_DATA_SEPARATOR + data);\n        }\n\n        private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash)\n        {\n            if (hash != localFileHash)\n                AddNotice(string.Format(\"{0} - modified files detected! They could be cheating!\".L10N(\"Client:Main:PlayerCheating\"), sender.Name), Color.Red);\n            sender.HashReceived = true;\n        }\n\n        private void Server_HandleReadyRequest(LANPlayerInfo sender)\n        {\n            if (!sender.Ready)\n            {\n                sender.Ready = true;\n                CopyPlayerDataToUI();\n                BroadcastOptions();\n            }\n        }\n\n        #endregion\n\n        #region Client's command handlers\n\n        private void Client_HandleChatMessage(string data)\n        {\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n\n            if (parts.Length < 3)\n                return;\n\n            string playerName = parts[0];\n\n            int colorIndex = Conversions.IntFromString(parts[1], -1);\n\n            if (colorIndex < 0 || colorIndex >= chatColors.Length)\n                return;\n\n            lbChatMessages.AddMessage(new ChatMessage(playerName,\n                chatColors[colorIndex].XNAColor, DateTime.Now, parts[2]));\n\n            sndMessageSound.Play();\n        }\n\n        private void Client_HandleOptionsMessage(string data)\n        {\n            if (IsHost)\n                return;\n\n            string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR);\n            const int PLAYER_INFO_PARTS = 3;\n            int pCount = (parts.Length - 1) / PLAYER_INFO_PARTS;\n\n            if (pCount * PLAYER_INFO_PARTS + 1 != parts.Length)\n                return;\n\n            int savedGameIndex = Conversions.IntFromString(parts[0], -1);\n            if (savedGameIndex < 0 || savedGameIndex >= ddSavedGame.Items.Count)\n            {\n                return;\n            }\n\n            ddSavedGame.SelectedIndex = savedGameIndex;\n\n            Players.Clear();\n\n            for (int i = 0; i < pCount; i++)\n            {\n                int baseIndex = 1 + i * PLAYER_INFO_PARTS;\n                string pName = parts[baseIndex];\n                bool ready = Conversions.IntFromString(parts[baseIndex + 1], -1) > 0;\n                string ipAddress = parts[baseIndex + 2];\n\n                LANPlayerInfo pInfo = new LANPlayerInfo(encoding);\n                pInfo.Name = pName;\n                pInfo.Ready = ready;\n                pInfo.IPAddress = ipAddress;\n                Players.Add(pInfo);\n            }\n\n            if (Players.Count > 0) // Set IP of host\n                Players[0].IPAddress = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();\n\n            CopyPlayerDataToUI();\n        }\n\n        private void Client_HandleStartCommand()\n        {\n            started = true;\n\n            LoadGame();\n        }\n\n        #endregion\n\n        /// <summary>\n        /// Broadcasts a command to all players in the game as the game host.\n        /// </summary>\n        /// <param name=\"message\">The command to send.</param>\n        private void BroadcastMessage(string message)\n        {\n            if (!IsHost)\n                return;\n\n            foreach (PlayerInfo pInfo in Players)\n            {\n                var lpInfo = (LANPlayerInfo)pInfo;\n                lpInfo.SendMessage(message);\n            }\n        }\n\n        private void SendMessageToHost(string message)\n        {\n            if (!client.Connected)\n                return;\n\n            byte[] buffer = encoding.GetBytes(\n                message + ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n            NetworkStream ns = client.GetStream();\n\n            try\n            {\n                ns.Write(buffer, 0, buffer.Length);\n                ns.Flush();\n            }\n            catch\n            {\n                Logger.Log(\"Sending message to game host failed!\");\n            }\n        }\n\n        public void SetChatColorIndex(int colorIndex)\n        {\n            chatColorIndex = colorIndex;\n        }\n\n        public override string GetSwitchName()\n        {\n            return \"Load Game\".L10N(\"Client:Main:LoadGameSwitchName\");\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (IsHost)\n            {\n                for (int i = 1; i < Players.Count; i++)\n                {\n                    LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i];\n                    if (!lpInfo.Update(gameTime))\n                    {\n                        CleanUpPlayer(lpInfo);\n                        Players.RemoveAt(i);\n                        AddNotice(string.Format(\"{0} - connection timed out\".L10N(\"Client:Main:PlayerTimeout\"), lpInfo.Name));\n                        CopyPlayerDataToUI();\n                        BroadcastOptions();\n                        UpdateDiscordPresence();\n                        i--;\n                    }\n                }\n\n                timeSinceGameBroadcast += gameTime.ElapsedGameTime;\n\n                if (timeSinceGameBroadcast > TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL))\n                {\n                    BroadcastGame();\n                    timeSinceGameBroadcast = TimeSpan.Zero;\n                }\n            }\n            else\n            {\n                timeSinceLastReceivedCommand += gameTime.ElapsedGameTime;\n\n                if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT))\n                {\n                    LobbyNotification?.Invoke(this,\n                        new LobbyNotificationEventArgs(\"Connection to the game host timed out.\".L10N(\"Client:Main:HostConnectTimeOut\")));\n                    LeaveGame();\n                }\n            }\n\n            base.Update(gameTime);\n        }\n\n        private void BroadcastGame()\n        {\n            var sb = new ExtendedStringBuilder(\"GAME \", true);\n            sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR;\n            sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION);\n            sb.Append(ProgramConstants.GAME_VERSION);\n            sb.Append(localGame);\n            sb.Append((string)lblMapNameValue.Tag);\n            sb.Append((string)lblGameModeValue.Tag);\n            sb.Append(0); // LoadedGameID\n            var sbPlayers = new StringBuilder();\n            SGPlayers.ForEach(p => sbPlayers.Append(p.Name + \",\"));\n            sbPlayers.Remove(sbPlayers.Length - 1, 1);\n            sb.Append(sbPlayers.ToString());\n            sb.Append(Convert.ToInt32(started || Players.Count == SGPlayers.Count));\n            sb.Append(1); // IsLoadedGame\n            sb.Append(string.Empty); // MapHash\n\n            GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString()));\n        }\n\n        protected override void HandleGameProcessExited()\n        {\n            base.HandleGameProcessExited();\n\n            LeaveGame();\n        }\n\n        protected override void UpdateDiscordPresence(bool resetTimer = false)\n        {\n            if (discordHandler == null)\n                return;\n\n            PlayerInfo player = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME);\n            if (player == null)\n                return;\n            string currentState = ProgramConstants.IsInGame ? \"In Game\" : \"In Lobby\"; // not UI strings\n\n            discordHandler.UpdatePresence(\n                (string)lblMapNameValue.Tag, (string)lblGameModeValue.Tag, currentState, \"LAN\",\n                Players.Count, SGPlayers.Count,\n                \"LAN Game\", IsHost, resetTimer);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANLobby.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Reflection;\nusing System.Text;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\n\nusing DTAClient.Domain;\nusing DTAClient.Domain.LAN;\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.Domain.Multiplayer.LAN;\nusing DTAClient.DXGUI.Multiplayer.CnCNet;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\nusing DTAClient.Online;\n\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nusing SixLabors.ImageSharp;\n\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    class LANLobby : XNAWindow\n    {\n        private const double ALIVE_MESSAGE_INTERVAL = 5.0;\n        private const double INACTIVITY_REMOVE_TIME = 10.0;\n        private const double GAME_INACTIVITY_REMOVE_TIME = 20.0;\n        private const double MESSAGE_ID_EXPIRATION_SECONDS = 60.0;\n\n        public LANLobby(\n            WindowManager windowManager,\n            GameCollection gameCollection,\n            MapLoader mapLoader,\n            DiscordHandler discordHandler,\n            Random random\n        ) : base(windowManager)\n        {\n            this.gameCollection = gameCollection;\n            this.mapLoader = mapLoader;\n            this.discordHandler = discordHandler;\n            this.random = random;\n        }\n\n        public event EventHandler Exited;\n\n        private Random random;\n\n        XNAClientButton btnMainMenu;\n        XNAClientButton btnNewGame;\n        XNAClientButton btnJoinGame;\n\n        XNAChatTextBox tbChatInput;\n\n        XNALabel lblColor;\n\n        XNAClientDropDown ddColor;\n\n        LANGameCreationWindow gameCreationWindow;\n\n        LANGameLobby lanGameLobby;\n\n        LANGameLoadingLobby lanGameLoadingLobby;\n\n        Texture2D unknownGameIcon;\n\n        LANColor[] chatColors;\n\n        string localGame;\n        int localGameIndex;\n\n        GameCollection gameCollection;\n\n        private IReadOnlyList<GameMode> gameModes => mapLoader.GameModes;\n\n        TimeSpan timeSinceGameRefresh = TimeSpan.Zero;\n\n        EnhancedSoundEffect sndGameCreated;\n\n        Encoding encoding;\n\n        ChatListBox lbChatMessages;\n\n        GameListBox lbGameList;\n\n        // lbPlayerList is now managed by LANPlayerManager `playerManager`\n        // XNAListBox lbPlayerList;\n        LANPlayerManager playerManager;\n\n        LANMessageDeduplicator messageDeduplicator;\n\n        LANLobbyBroadcastManager broadcastManager;\n\n        TimeSpan timeSinceAliveMessage = TimeSpan.Zero;\n\n        MapLoader mapLoader;\n\n        DiscordHandler discordHandler;\n        PrivateMessagingWindow pmWindow;\n\n        public override void Initialize()\n        {\n            Name = \"LANLobby\";\n            BackgroundTexture = AssetLoader.LoadTexture(\"cncnetlobbybg.png\");\n            ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 64,\n                WindowManager.RenderResolutionY - 64);\n\n            localGame = ClientConfiguration.Instance.LocalGame;\n            localGameIndex = gameCollection.GameList.FindIndex(\n                g => g.InternalName.ToUpper() == localGame.ToUpper());\n\n            btnNewGame = new XNAClientButton(WindowManager);\n            btnNewGame.Name = \"btnNewGame\";\n            btnNewGame.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnNewGame.Text = \"Create Game\".L10N(\"Client:Main:CreateGame\");\n            btnNewGame.LeftClick += BtnNewGame_LeftClick;\n\n            btnJoinGame = new XNAClientButton(WindowManager);\n            btnJoinGame.Name = \"btnJoinGame\";\n            btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12,\n                btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnJoinGame.Text = \"Join Game\".L10N(\"Client:Main:JoinGame\");\n            btnJoinGame.LeftClick += BtnJoinGame_LeftClick;\n\n            btnMainMenu = new XNAClientButton(WindowManager);\n            btnMainMenu.Name = \"btnMainMenu\";\n            btnMainMenu.ClientRectangle = new Rectangle(Width - 145,\n                btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT);\n            btnMainMenu.Text = \"Main Menu\".L10N(\"Client:Main:MainMenu\");\n            btnMainMenu.LeftClick += BtnMainMenu_LeftClick;\n\n            lbGameList = new GameListBox(WindowManager, mapLoader, localGame);\n            lbGameList.Name = \"lbGameList\";\n            lbGameList.ClientRectangle = new Rectangle(btnNewGame.X,\n                41, btnJoinGame.Right - btnNewGame.X,\n                btnNewGame.Y - 53);\n            lbGameList.GameLifetime = 15.0; // Smaller lifetime in LAN\n            lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick;\n            lbGameList.AllowMultiLineItems = false;\n\n            var lbPlayerList = new XNAListBox(WindowManager);\n            lbPlayerList.Name = \"lbPlayerList\";\n            lbPlayerList.ClientRectangle = new Rectangle(Width - 202,\n                lbGameList.Y, 190,\n                lbGameList.Height);\n            lbPlayerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbPlayerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbPlayerList.LineHeight = 16;\n\n            lbChatMessages = new ChatListBox(WindowManager);\n            lbChatMessages.Name = \"lbChatMessages\";\n            lbChatMessages.ClientRectangle = new Rectangle(lbGameList.Right + 12,\n                lbGameList.Y,\n                lbPlayerList.X - lbGameList.Right - 24,\n                lbGameList.Height);\n            lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;\n            lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1);\n            lbChatMessages.LineHeight = 16;\n\n            tbChatInput = new XNAChatTextBox(WindowManager);\n            tbChatInput.Name = \"tbChatInput\";\n            tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X,\n                btnNewGame.Y, lbChatMessages.Width,\n                btnNewGame.Height);\n            tbChatInput.Suggestion = \"Type here to chat...\".L10N(\"Client:Main:ChatHere\");\n            tbChatInput.MaximumTextLength = 200;\n            tbChatInput.EnterPressed += TbChatInput_EnterPressed;\n\n            lblColor = new XNALabel(WindowManager);\n            lblColor.Name = \"lblColor\";\n            lblColor.ClientRectangle = new Rectangle(lbChatMessages.X, 14, 0, 0);\n            lblColor.FontIndex = 1;\n            lblColor.Text = \"YOUR COLOR:\".L10N(\"Client:Main:YourColor\");\n\n            ddColor = new XNAClientDropDown(WindowManager);\n            ddColor.Name = \"ddColor\";\n            ddColor.ClientRectangle = new Rectangle(lblColor.X + 95, 12,\n                150, 21);\n\n            chatColors = new LANColor[]\n            {\n                new LANColor(\"Gray\".L10N(\"Client:Main:ColorGray\"), Color.Gray),\n                new LANColor(\"Metallic\".L10N(\"Client:Main:ColorLightGrayMetallic\"), Color.LightGray),\n                new LANColor(\"Green\".L10N(\"Client:Main:ColorGreen\"), Color.ForestGreen),\n                new LANColor(\"Lime Green\".L10N(\"Client:Main:ColorLimeGreen\"), Color.LimeGreen),\n                new LANColor(\"Green Yellow\".L10N(\"Client:Main:ColorGreenYellow\"), Color.GreenYellow),\n                new LANColor(\"Goldenrod\".L10N(\"Client:Main:ColorGoldenrod\"), Color.Goldenrod),\n                new LANColor(\"Yellow\".L10N(\"Client:Main:ColorYellow\"), Color.Yellow),\n                new LANColor(\"Orange\".L10N(\"Client:Main:ColorOrange\"), Color.Orange),\n                new LANColor(\"Red\".L10N(\"Client:Main:ColorRed\"), Color.Red),\n                new LANColor(\"Pink\".L10N(\"Client:Main:ColorPink\"), Color.DeepPink),\n                new LANColor(\"Purple\".L10N(\"Client:Main:ColorPurple\"), Color.MediumPurple),\n                new LANColor(\"Sky Blue\".L10N(\"Client:Main:ColorSkyBlue\"), Color.LightSkyBlue),\n                new LANColor(\"Blue\".L10N(\"Client:Main:ColorBlue\"), Color.RoyalBlue),\n                new LANColor(\"Brown\".L10N(\"Client:Main:ColorBrown\"), Color.SaddleBrown),\n                new LANColor(\"Teal\".L10N(\"Client:Main:ColorTeal\"), Color.Teal)\n            };\n\n            foreach (LANColor color in chatColors)\n            {\n                ddColor.AddItem(color.Name, color.XNAColor);\n            }\n\n            AddChild(btnNewGame);\n            AddChild(btnJoinGame);\n            AddChild(btnMainMenu);\n\n            AddChild(lbPlayerList);\n            AddChild(lbChatMessages);\n            AddChild(lbGameList);\n            AddChild(tbChatInput);\n            AddChild(lblColor);\n            AddChild(ddColor);\n\n            gameCreationWindow = new LANGameCreationWindow(WindowManager);\n            var gameCreationPanel = new DarkeningPanel(WindowManager);\n            AddChild(gameCreationPanel);\n            gameCreationPanel.AddChild(gameCreationWindow);\n            gameCreationWindow.Disable();\n\n            gameCreationWindow.NewGame += GameCreationWindow_NewGame;\n            gameCreationWindow.LoadGame += GameCreationWindow_LoadGame;\n\n            // Initialize player manager after lbPlayerList is created\n            playerManager = new LANPlayerManager(lbPlayerList);\n\n            // Initialize message deduplicator with a random seed\n            messageDeduplicator = new LANMessageDeduplicator(random.Next(), MESSAGE_ID_EXPIRATION_SECONDS);\n\n            // Initialize broadcast manager\n            encoding = Encoding.UTF8;\n            broadcastManager = new LANLobbyBroadcastManager(ProgramConstants.LAN_LOBBY_PORT, encoding);\n            // Dispatch to UI thread\n            broadcastManager.MessageReceived += (sender, e) =>\n                AddCallback(() => HandleNetworkMessage(e.Data, e.EndPoint));\n\n            var assembly = Assembly.GetAssembly(typeof(GameCollection));\n            using Stream unknownIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.unknownicon.png\");\n\n            unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream));\n\n            sndGameCreated = new EnhancedSoundEffect(\"gamecreated.wav\");\n\n            base.Initialize();\n\n            CenterOnParent();\n            gameCreationPanel.SetPositionAndSize();\n\n            lanGameLobby = new LANGameLobby(WindowManager, \"MultiplayerGameLobby\",\n                null, chatColors, mapLoader, discordHandler, pmWindow, random);\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, lanGameLobby);\n            lanGameLobby.Disable();\n\n            lanGameLoadingLobby = new LANGameLoadingLobby(WindowManager,\n                chatColors, mapLoader, discordHandler);\n            DarkeningPanel.AddAndInitializeWithControl(WindowManager, lanGameLoadingLobby);\n            lanGameLoadingLobby.Disable();\n\n            int selectedColor = UserINISettings.Instance.LANChatColor;\n\n            ddColor.SelectedIndex = selectedColor >= ddColor.Items.Count || selectedColor < 0\n                ? 0 : selectedColor;\n\n            SetChatColor();\n            ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged;\n\n            lanGameLobby.GameLeft += LanGameLobby_GameLeft;\n            lanGameLobby.GameBroadcast += LanGameLobby_GameBroadcast;\n\n            lanGameLoadingLobby.GameBroadcast += LanGameLoadingLobby_GameBroadcast;\n            lanGameLoadingLobby.GameLeft += LanGameLoadingLobby_GameLeft;\n\n            WindowManager.GameClosing += WindowManager_GameClosing;\n        }\n\n        private void LanGameLoadingLobby_GameLeft(object sender, EventArgs e)\n        {\n            Enable();\n        }\n\n        private void WindowManager_GameClosing(object sender, EventArgs e)\n        {\n            SendMessage(\"QUIT\");\n\n            // Dispose the broadcast manager (which closes the socket and stops the listener)\n            broadcastManager?.Dispose();\n\n            // Dispose the message deduplicator to stop the cleanup timer\n            messageDeduplicator?.Dispose();\n        }\n\n        private void LanGameLobby_GameBroadcast(object sender, GameBroadcastEventArgs e)\n        {\n            SendMessage(e.Message);\n        }\n\n        private void LanGameLobby_GameLeft(object sender, GameLeftEventArgs e)\n        {\n            if (!string.IsNullOrWhiteSpace(e.Message))\n                AddChatMessage(new ChatMessage(Color.Red, e.Message));\n\n            Enable();\n        }\n\n        private void LanGameLoadingLobby_GameBroadcast(object sender, GameBroadcastEventArgs e)\n        {\n            SendMessage(e.Message);\n        }\n\n        private void GameCreationWindow_LoadGame(object sender, GameLoadEventArgs e)\n        {\n            lanGameLoadingLobby.SetUp(true,\n                new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT),\n                null, e.LoadedGameID);\n\n            lanGameLoadingLobby.Enable();\n        }\n\n        private void GameCreationWindow_NewGame(object sender, EventArgs e)\n        {\n            lanGameLobby.SetUp(true,\n                new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null);\n\n            lanGameLobby.Enable();\n        }\n\n        private void SetChatColor()\n        {\n            tbChatInput.TextColor = chatColors[ddColor.SelectedIndex].XNAColor;\n            lanGameLobby.SetChatColorIndex(ddColor.SelectedIndex);\n            UserINISettings.Instance.LANChatColor.Value = ddColor.SelectedIndex;\n        }\n\n        private void DdColor_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            SetChatColor();\n            UserINISettings.Instance.SaveSettings();\n        }\n\n        void AddChatMessage(ChatMessage message)\n            => lbChatMessages.AddMessage(message);\n\n        void AddChatMessage(string message)\n            => lbChatMessages.AddMessage(message);\n\n        void AddChatMessage(string sender, string message, Color color)\n            => lbChatMessages.AddMessage(sender, message, color);\n\n        public void Open()\n        {\n            playerManager.Clear();\n            messageDeduplicator.Clear();\n            lbGameList.ClearGames();\n\n            Visible = true;\n            Enabled = true;\n\n            try\n            {\n                broadcastManager.Initialize();\n            }\n            catch (Exception ex)\n            {\n                AddChatMessage(new ChatMessage(Color.Red,\n                    \"Creating LAN socket failed! Message:\".L10N(\"Client:Main:SocketFailure1\") + \" \" + ex.Message + \"\\n\" +\n                    \"Please check your firewall settings.\".L10N(\"Client:Main:SocketFailure2\") + \" \" +\n                    \"Also make sure that no other application is listening to traffic on UDP ports 1232 - 1234.\".L10N(\"Client:Main:SocketFailure3\")));\n\n                return;\n            }\n\n            SendAlive();\n        }\n\n        private void SendMessage(string message)\n        {\n            // Wrap message with message ID at the beginning\n            string wrappedMessage = messageDeduplicator.WrapMessage(message);\n\n            bool sendSucceeded = broadcastManager.SendMessage(wrappedMessage);\n\n            if (!sendSucceeded)\n            {\n                // Socket is not initialized or sending failed; report this so failures are not silent.\n                AddChatMessage(new ChatMessage(Color.Red,\n                        \"Failed to send LAN broadcast message. The network socket may not be initialized.\"));\n            }\n        }\n\n        private void HandleNetworkMessage(string data, IPEndPoint endPoint)\n        {\n            // Unwrap message to extract message ID and check for duplicates\n            messageDeduplicator.UnwrapMessage(data, out string payload, out bool isDuplicate);\n\n            if (isDuplicate || string.IsNullOrWhiteSpace(payload))\n                return;\n\n            string[] commandAndParams = payload.Split(' ');\n\n            string command = commandAndParams[0];\n\n            string[] parameters;\n            {\n                // For parameterless commands like \"QUIT\", avoid the potential out-of-bounds issue by locating the first space first.\n                int firstSpace = payload.IndexOf(' ');\n                parameters = firstSpace >= 0\n                    ? payload.Substring(firstSpace + 1).Split([ProgramConstants.LAN_DATA_SEPARATOR])\n                    : [];\n            }\n\n            LANLobbyUser user = playerManager.GetPlayerIfExist(endPoint);\n\n            switch (command)\n            {\n                case \"ALIVE\":\n                    if (parameters.Length < 2)\n                        return;\n\n                    int gameIndex = Conversions.IntFromString(parameters[0], -1);\n                    string name = parameters[1];\n\n                    if (user == null)\n                    {\n                        Texture2D gameTexture = unknownGameIcon;\n\n                        if (gameIndex > -1 && gameIndex < gameCollection.GameList.Count)\n                            gameTexture = gameCollection.GameList[gameIndex].Texture;\n\n                        user = playerManager.GetOrCreatePlayer(endPoint, name, gameTexture);\n                    }\n\n                    user.ClearTimeWithoutRefresh();\n\n                    break;\n\n                case \"CHAT\":\n                    if (user == null)\n                        return;\n\n                    if (parameters.Length < 2)\n                        return;\n\n                    int colorIndex = Conversions.IntFromString(parameters[0], -1);\n\n                    if (colorIndex < 0 || colorIndex >= chatColors.Length)\n                        return;\n\n                    AddChatMessage(new ChatMessage(user.Name,\n                        chatColors[colorIndex].XNAColor, DateTime.Now, parameters[1]));\n\n                    break;\n\n                case \"QUIT\":\n                    if (user == null)\n                        return;\n\n                    playerManager.RemovePlayer(endPoint);\n\n                    break;\n\n                case \"GAMECLOSED\":\n                    int closedGameIndex = lbGameList.HostedGames.FindIndex(g => ((HostedLANGame)g).EndPoint.Equals(endPoint));\n                    if (closedGameIndex > -1)\n                    {\n                        lbGameList.HostedGames.RemoveAt(closedGameIndex);\n                        lbGameList.Refresh();\n                    }\n\n                    break;\n\n                case \"GAME\":\n                    if (user == null)\n                        return;\n\n                    HostedLANGame game = new HostedLANGame();\n                    if (!game.SetDataFromStringArray(gameCollection, parameters))\n                        return;\n\n                    game.EndPoint = endPoint;\n\n                    int existingGameIndex =\n                        lbGameList.HostedGames.FindIndex(g => ((HostedLANGame)g).EndPoint.Equals(endPoint));\n\n                    if (existingGameIndex > -1)\n                        lbGameList.HostedGames[existingGameIndex] = game;\n                    else\n                        lbGameList.HostedGames.Add(game);\n\n                    lbGameList.Refresh();\n\n                    break;\n            }\n        }\n\n        private void SendAlive()\n        {\n            StringBuilder sb = new StringBuilder(\"ALIVE \");\n            sb.Append(localGameIndex);\n            sb.Append(ProgramConstants.LAN_DATA_SEPARATOR);\n            sb.Append(ProgramConstants.PLAYERNAME);\n            SendMessage(sb.ToString());\n            timeSinceAliveMessage = TimeSpan.Zero;\n        }\n\n        private void TbChatInput_EnterPressed(object sender, EventArgs e)\n        {\n            if (string.IsNullOrEmpty(tbChatInput.Text))\n                return;\n\n            string chatMessage = tbChatInput.Text.Replace((char)01, '?');\n\n            StringBuilder sb = new StringBuilder(\"CHAT \");\n            sb.Append(ddColor.SelectedIndex);\n            sb.Append(ProgramConstants.LAN_DATA_SEPARATOR);\n            sb.Append(chatMessage);\n\n            SendMessage(sb.ToString());\n\n            tbChatInput.Text = string.Empty;\n        }\n\n        private void LbGameList_DoubleLeftClick(object sender, EventArgs e)\n        {\n            if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count)\n                return;\n\n            HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag;\n\n            if (hg.Game.InternalName.ToUpper() != localGame.ToUpper())\n            {\n                AddChatMessage(\n                    string.Format(\"The selected game is for {0}!\".L10N(\"Client:Main:GameIsOfPurpose\"),\n                    gameCollection.GetGameNameFromInternalName(hg.Game.InternalName)));\n                return;\n            }\n\n            if (hg.Locked)\n            {\n                AddChatMessage(string.Format(\"The game {0} is locked!\".L10N(\"Client:Main:GameLockedWithName\"), hg.RoomName));\n                return;\n            }\n\n            if (hg.IsLoadedGame)\n            {\n                if (!hg.Players.Contains(ProgramConstants.PLAYERNAME))\n                {\n                    AddChatMessage(\"You do not exist in the saved game!\".L10N(\"Client:Main:NotInSavedGame\"));\n                    return;\n                }\n            }\n            else\n            {\n                if (hg.Players.Contains(ProgramConstants.PLAYERNAME))\n                {\n                    AddChatMessage(\"Your name is already taken in the game.\".L10N(\"Client:Main:NameOccupied\"));\n                    return;\n                }\n            }\n\n            if (hg.GameVersion != ProgramConstants.GAME_VERSION)\n            {\n                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\")));\n            }\n\n            AddChatMessage(string.Format(\"Attempting to join game {0} ...\".L10N(\"Client:Main:AttemptJoin\"), hg.RoomName));\n\n            try\n            {\n                var client = new TcpClient(hg.EndPoint.Address.ToString(), ProgramConstants.LAN_GAME_LOBBY_PORT);\n\n                byte[] buffer;\n\n                if (hg.IsLoadedGame)\n                {\n                    var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI));\n\n                    int loadedGameId = spawnSGIni.GetIntValue(\"Settings\", \"GameID\", -1);\n\n                    lanGameLoadingLobby.SetUp(false, hg.EndPoint, client, loadedGameId);\n                    lanGameLoadingLobby.Enable();\n\n                    buffer = encoding.GetBytes(\"JOIN\" + ProgramConstants.LAN_DATA_SEPARATOR +\n                        ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR +\n                        loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n                    client.GetStream().Write(buffer, 0, buffer.Length);\n                    client.GetStream().Flush();\n\n                    lanGameLoadingLobby.PostJoin();\n                }\n                else\n                {\n                    lanGameLobby.SetUp(false, hg.EndPoint, client);\n                    lanGameLobby.Enable();\n\n                    buffer = encoding.GetBytes(\"JOIN\" + ProgramConstants.LAN_DATA_SEPARATOR +\n                        ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n                    client.GetStream().Write(buffer, 0, buffer.Length);\n                    client.GetStream().Flush();\n\n                    lanGameLobby.PostJoin();\n                }\n            }\n            catch (Exception ex)\n            {\n                AddChatMessage(null,\n                    \"Connecting to the game failed! Message:\".L10N(\"Client:Main:ConnectGameFailed\") + \" \" + ex.Message, Color.White);\n            }\n        }\n\n        private void BtnMainMenu_LeftClick(object sender, EventArgs e)\n        {\n            Visible = false;\n            Enabled = false;\n            SendMessage(\"QUIT\");\n            broadcastManager.Shutdown();\n            Exited?.Invoke(this, EventArgs.Empty);\n        }\n\n        private void BtnJoinGame_LeftClick(object sender, EventArgs e)\n        {\n            LbGameList_DoubleLeftClick(this, EventArgs.Empty);\n        }\n\n        private void BtnNewGame_LeftClick(object sender, EventArgs e)\n        {\n            if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading)\n                gameCreationWindow.Open();\n            else\n                GameCreationWindow_NewGame(sender, e);\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            // Remove inactive players periodically\n            {\n                // Get a thread-safe snapshot of all players\n                var playersCopy = playerManager.GetAllPlayers();\n                foreach (var player in playersCopy)\n                {\n                    player.AddToTimeWithoutRefresh(gameTime.ElapsedGameTime);\n\n                    if (player.TimeWithoutRefresh > TimeSpan.FromSeconds(INACTIVITY_REMOVE_TIME))\n                        playerManager.RemovePlayer(player.EndPoint);\n                }\n            }\n\n            // Send ALIVE message periodically\n            timeSinceAliveMessage += gameTime.ElapsedGameTime;\n            if (timeSinceAliveMessage > TimeSpan.FromSeconds(ALIVE_MESSAGE_INTERVAL))\n                SendAlive();\n\n            base.Update(gameTime);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANLobbyBroadcastManager.cs",
    "content": "#nullable enable\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\n\nusing Rampastring.Tools;\n\nusing NetworkInterface = System.Net.NetworkInformation.NetworkInterface;\n\nnamespace DTAClient.DXGUI.Multiplayer;\n\n/// <summary>\n/// Thread-safe manager for LAN lobby broadcasting and network communication.\n/// Encapsulates socket management, broadcast interface discovery, message sending,\n/// and network listening to ensure thread-safe operations.\n/// \n/// This class broadcasts messages to all available network interfaces, and provides a message ID based de-duplication mechanism.\n/// By broadcasting to all interfaces, it ensures that messages reach all clients.\n/// 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.\n/// </summary>\ninternal class LANLobbyBroadcastManager : IDisposable\n{\n    private readonly object socketLock = new();\n    private readonly ConcurrentDictionary<string, PlayerNetworkInterface> broadcastInterfaces = new();\n    private readonly Encoding encoding;\n    private readonly int lobbyPort;\n\n    private Socket? socket;\n    private Thread? listener;\n    private Thread? interfaceRefresher;\n    private volatile bool stopRefresher = false;\n    private int disposed = 0;\n\n    /// <summary>\n    /// Event raised when a network message is received.\n    /// The event is raised on the listener thread; subscribers are responsible\n    /// for marshaling to the main/UI thread if required.\n    /// </summary>\n    public event EventHandler<LANLobbyBroadcastMessageReceivedEventArgs>? MessageReceived;\n\n    /// <summary>\n    /// Record for storing network interface information.\n    /// </summary>\n    /// <param name=\"LocalIP\">The local IP address of this interface.</param>\n    /// <param name=\"Broadcast\">The broadcast endpoint for this interface.</param>\n    private record PlayerNetworkInterface(IPAddress LocalIP, IPEndPoint Broadcast);\n\n    /// <summary>\n    /// Initializes a new instance of the LANLobbyBroadcastManager class.\n    /// </summary>\n    /// <param name=\"lobbyPort\">The UDP port to bind for LAN lobby communication.</param>\n    /// <param name=\"encoding\">The text encoding to use for messages (typically UTF-8).</param>\n    public LANLobbyBroadcastManager(int lobbyPort, Encoding encoding)\n    {\n        this.lobbyPort = lobbyPort;\n        this.encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));\n    }\n\n    /// <summary>\n    /// Gets whether the socket is successfully initialized and bound.\n    /// </summary>\n    public bool IsInitialized\n    {\n        get\n        {\n            lock (socketLock)\n            {\n                return socket != null && socket.IsBound;\n            }\n        }\n    }\n\n    /// <summary>\n    /// Initializes the socket, binds it to the lobby port, and starts listening for messages.\n    /// </summary>\n    public void Initialize()\n    {\n        lock (socketLock)\n        {\n            // Clean up any existing socket\n            if (socket != null)\n            {\n                try\n                {\n                    socket.Close();\n                }\n                catch (ObjectDisposedException)\n                {\n                    // Already disposed\n                }\n                socket = null;\n            }\n\n            // Clear broadcast interfaces\n            broadcastInterfaces.Clear();\n\n            Logger.Log(\"Creating LAN socket.\");\n\n            try\n            {\n                socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)\n                {\n                    EnableBroadcast = true\n                };\n                socket.Bind(new IPEndPoint(IPAddress.Any, lobbyPort));\n                \n                // Discover initial broadcast interfaces\n                var initialInterfaces = DiscoverBroadcastInterfaces(lobbyPort);\n                foreach (var (key, netIf) in initialInterfaces)\n                {\n                    broadcastInterfaces[key] = netIf;\n                }\n            }\n            catch (SocketException ex)\n            {\n                Logger.Log(\"Creating LAN socket failed! Message: \" + ex.ToString());\n                throw;\n            }\n\n            // Reset stop flag for the refresher thread\n            stopRefresher = false;\n\n            Logger.Log(\"Starting LAN broadcast message listener.\");\n            listener = new Thread(new ThreadStart(Listen))\n            {\n                IsBackground = true\n            };\n            listener.Start();\n\n            Logger.Log(\"Starting network interface refresh thread.\");\n            interfaceRefresher = new Thread(new ThreadStart(RefreshInterfacesPeriodically))\n            {\n                IsBackground = true\n            };\n            interfaceRefresher.Start();\n        }\n    }\n\n    /// <summary>\n    /// Discovers all available network interfaces for broadcasting.\n    /// This method scans only \"up\" network interfaces and identifies those with valid IPv4 addresses.\n    /// </summary>\n    /// <param name=\"port\">The port to use for broadcast endpoints.</param>\n    /// <returns>A dictionary of network interfaces keyed by their local IP address.</returns>\n    private static Dictionary<string, PlayerNetworkInterface> DiscoverBroadcastInterfaces(int port)\n    {\n        Logger.Log(\"Discovering broadcast interfaces.\");\n\n        var discoveredInterfaces = new Dictionary<string, PlayerNetworkInterface>();\n        NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();\n        foreach (NetworkInterface iface in interfaces)\n        {\n            // Only consider interfaces that are operational (up)\n            if (iface.OperationalStatus != OperationalStatus.Up)\n                continue;\n\n            IPInterfaceProperties prop = iface.GetIPProperties();\n            UnicastIPAddressInformation? info = prop.UnicastAddresses.FirstOrDefault(info =>\n                info.Address.AddressFamily == AddressFamily.InterNetwork);\n\n            if (info == null || info.IPv4Mask == null)\n                continue;\n\n            IPAddress localIPAddress = info.Address;\n            byte[] ipBytes = localIPAddress.GetAddressBytes();\n            byte[] maskBytes = info.IPv4Mask.GetAddressBytes();\n            byte[] broadcastBytes = new byte[ipBytes.Length];\n            for (int i = 0; i < ipBytes.Length; i++)\n            {\n                broadcastBytes[i] = (byte)(ipBytes[i] | ~maskBytes[i]);\n            }\n            IPAddress broadcastIP = new IPAddress(broadcastBytes);\n\n            string key = localIPAddress.ToString();\n            var netIf = new PlayerNetworkInterface(localIPAddress, new IPEndPoint(broadcastIP, port));\n            discoveredInterfaces[key] = netIf;\n        }\n\n        if (discoveredInterfaces.Count == 0)\n        {\n            Logger.Log(\"Warning: No broadcast interfaces found! LAN lobby broadcasting will not function. \" +\n                \"Please ensure that your network adapters are enabled and have valid IPv4 addresses.\");\n        }\n\n        return discoveredInterfaces;\n    }\n\n    /// <summary>\n    /// Sends a message to all broadcast interfaces.\n    /// Failed interfaces are logged but not removed from the broadcast list.\n    /// </summary>\n    /// <param name=\"message\">The message to broadcast.</param>\n    /// <returns>True if the message was sent successfully to at least one interface, false if the socket is not initialized or all interfaces fail.</returns>\n    public bool SendMessage(string message)\n    {\n        lock (socketLock)\n        {\n            if (socket == null || !socket.IsBound)\n                return false;\n\n            byte[] buffer = encoding.GetBytes(message);\n\n            if (broadcastInterfaces.IsEmpty)\n            {\n                Logger.Log(\"Warning: No broadcast interfaces available in SendMessage!\");\n            }\n\n            bool success = false;\n            foreach ((string key, PlayerNetworkInterface networkInterface) in broadcastInterfaces)\n            {\n                try\n                {\n                    _ = socket.SendTo(buffer, networkInterface.Broadcast);\n                    success = true;\n                }\n                catch (SocketException)\n                {\n                    // Do nothing\n                }\n            }\n\n            return success;\n        }\n    }\n\n    /// <summary>\n    /// Background thread that listens for incoming UDP messages.\n    /// </summary>\n    private void Listen()\n    {\n        try\n        {\n            while (true)\n            {\n                Socket? currentSocket;\n                lock (socketLock)\n                {\n                    currentSocket = socket;\n                }\n\n                if (currentSocket == null)\n                    break;\n\n                EndPoint endPoint = new IPEndPoint(IPAddress.Any, lobbyPort);\n                byte[] buffer = new byte[4096];\n                int receivedBytes = currentSocket.ReceiveFrom(buffer, ref endPoint);\n\n                IPEndPoint ipEndPoint = (IPEndPoint)endPoint;\n                string data = encoding.GetString(buffer, 0, receivedBytes);\n\n                if (string.IsNullOrEmpty(data))\n                    continue;\n\n                HandleNetworkMessage(data, ipEndPoint);\n            }\n        }\n        catch (Exception ex)\n        {\n            if (ex is SocketException socketEx && socketEx.SocketErrorCode == SocketError.Interrupted)\n            {\n                // Do nothing; this is the expected way for the listener thread to end.\n            }\n            else\n            {\n                Logger.Log(\"LAN socket listener: exception: \" + ex.ToString());\n            }\n        }\n    }\n\n    /// <summary>\n    /// Handles a received network message by raising the MessageReceived event.\n    /// </summary>\n    private void HandleNetworkMessage(string data, IPEndPoint endPoint)\n    {\n        MessageReceived?.Invoke(this, new LANLobbyBroadcastMessageReceivedEventArgs(data, endPoint));\n    }\n\n    /// <summary>\n    /// Interval in milliseconds for refreshing network interfaces.\n    /// </summary>\n    private const int INTERFACE_REFRESH_INTERVAL_MS = 5000;\n\n    /// <summary>\n    /// Interval in milliseconds for checking the stop signal during sleep.\n    /// </summary>\n    private const int STOP_CHECK_INTERVAL_MS = 100;\n\n    /// <summary>\n    /// Background thread that periodically refreshes network interfaces.\n    /// This ensures that the broadcast list stays up-to-date with network changes.\n    /// </summary>\n    private void RefreshInterfacesPeriodically()\n    {\n        try\n        {\n            while (!stopRefresher)\n            {\n                // Sleep for the refresh interval, but check periodically for stop signal\n                int iterations = INTERFACE_REFRESH_INTERVAL_MS / STOP_CHECK_INTERVAL_MS;\n                for (int i = 0; i < iterations && !stopRefresher; i++)\n                    Thread.Sleep(STOP_CHECK_INTERVAL_MS);\n\n                if (stopRefresher)\n                    break;\n\n                // Check if we're disposed\n                if (Volatile.Read(ref disposed) != 0)\n                    break;\n\n                lock (socketLock)\n                {\n                    // Check stop flag again inside lock to avoid race condition\n                    if (stopRefresher)\n                        break;\n\n                    // Check if socket is still valid\n                    if (socket == null || !socket.IsBound)\n                        break;\n                }\n\n                // Discover new interfaces outside the lock to minimize lock time\n                var newInterfaces = DiscoverBroadcastInterfaces(lobbyPort);\n\n                lock (socketLock)\n                {\n                    // Check again after discovery in case state changed\n                    if (stopRefresher || socket == null || !socket.IsBound)\n                        break;\n\n                    broadcastInterfaces.Clear();\n                    foreach (var (key, netIf) in newInterfaces)\n                    {\n                        broadcastInterfaces[key] = netIf;\n                    }\n                }\n            }\n        }\n        catch (ThreadInterruptedException)\n        {\n            // Expected when shutting down\n        }\n        catch (Exception ex)\n        {\n            Logger.Log(\"Network interface refresh thread: exception: \" + ex.ToString());\n        }\n    }\n\n    /// <summary>\n    /// Timeout in milliseconds for waiting for threads to terminate during shutdown.\n    /// </summary>\n    private const int THREAD_SHUTDOWN_TIMEOUT_MS = 1000;\n\n    /// <summary>\n    /// Closes the socket and stops the listening thread.\n    /// </summary>\n    public void Shutdown()\n    {\n        lock (socketLock)\n        {\n            // Signal the refresher thread to stop (inside lock for thread safety)\n            stopRefresher = true;\n\n            if (socket != null && socket.IsBound)\n            {\n                try\n                {\n                    socket.Close();\n                }\n                catch (ObjectDisposedException)\n                {\n                    // Already disposed\n                }\n            }\n\n            socket = null;\n        }\n\n        if (listener != null)\n        {\n            bool listenerTerminated = listener.Join(millisecondsTimeout: THREAD_SHUTDOWN_TIMEOUT_MS);\n            if (!listenerTerminated)\n                Logger.Log(\"Failed to shut down listener after timeout!\");\n\n            listener = null;\n        }\n\n        if (interfaceRefresher != null)\n        {\n            // Interrupt the thread to wake it from sleep\n            interfaceRefresher.Interrupt();\n            bool refresherTerminated = interfaceRefresher.Join(millisecondsTimeout: THREAD_SHUTDOWN_TIMEOUT_MS);\n            if (!refresherTerminated)\n                Logger.Log(\"Failed to shut down interface refresher after timeout!\");\n\n            interfaceRefresher = null;\n        }\n\n        // Clear broadcast interfaces\n        broadcastInterfaces.Clear();\n    }\n\n    /// <summary>\n    /// Gets the count of active broadcast interfaces.\n    /// </summary>\n    public int BroadcastInterfaceCount => broadcastInterfaces.Count;\n\n    /// <summary>\n    /// Disposes the broadcast manager and releases all resources.\n    /// </summary>\n    public void Dispose()\n    {\n        if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0)\n            Shutdown();\n\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANLobbyBroadcastMessageReceivedEventArgs.cs",
    "content": "#nullable enable\nusing System;\nusing System.Net;\n\nnamespace DTAClient.DXGUI.Multiplayer;\n\n/// <summary>\n/// Event arguments for network message received events.\n/// </summary>\ninternal class LANLobbyBroadcastMessageReceivedEventArgs : EventArgs\n{\n    /// <summary>\n    /// The received message data.\n    /// </summary>\n    public string Data { get; }\n\n    /// <summary>\n    /// The endpoint from which the message was received.\n    /// </summary>\n    public IPEndPoint EndPoint { get; }\n\n    public LANLobbyBroadcastMessageReceivedEventArgs(string data, IPEndPoint endPoint)\n    {\n        Data = data;\n        EndPoint = endPoint;\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANMessageDeduplicator.cs",
    "content": "#nullable enable\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Threading;\n\nnamespace DTAClient.DXGUI.Multiplayer;\n\n/// <summary>\n/// Thread-safe message de-duplicator for LAN lobby messages.\n/// Generates unique message IDs for outgoing messages and tracks received message IDs\n/// to filter out duplicates. Message IDs expire after a configurable timeout to prevent\n/// memory leaks. Cleanup is performed automatically in a background thread.\n/// </summary>\ninternal class LANMessageDeduplicator : IDisposable\n{\n    private readonly Random random;\n    private readonly object lockObject = new();\n\n    // Track received message IDs with their expiration time\n    private readonly ConcurrentDictionary<string, DateTime> receivedMessageIds = new();\n\n    // Message ID expiration time in seconds\n    private readonly double messageIdExpirationSeconds;\n\n    // Background cleanup\n    private readonly Timer cleanupTimer;\n    private const double CLEANUP_INTERVAL_SECONDS = 30.0;\n\n    private int disposed = 0;\n\n    /// <summary>\n    /// Initializes a new instance of the LANMessageDeduplicator class.\n    /// </summary>\n    /// <param name=\"randomSeed\">Seed for the random number generator used to create message IDs.</param>\n    /// <param name=\"messageIdExpirationSeconds\">How long to keep message IDs before expiring them (default 60 seconds).</param>\n    public LANMessageDeduplicator(int randomSeed, double messageIdExpirationSeconds = 60.0)\n    {\n        random = new Random(randomSeed);\n        this.messageIdExpirationSeconds = messageIdExpirationSeconds;\n\n        // Start automatic cleanup timer\n        int cleanupIntervalMs = (int)(CLEANUP_INTERVAL_SECONDS * 1000);\n        cleanupTimer = new Timer(CleanupCallback, null, cleanupIntervalMs, cleanupIntervalMs);\n    }\n\n    private void CleanupCallback(object? state)\n    {\n        if (disposed == 0)\n            CleanupExpiredMessageIds();\n    }\n\n    /// <summary>\n    /// Generates a unique random message ID.\n    /// Message IDs are prefixed with \"MID_\" followed by 8 alphanumeric characters\n    /// to avoid collision with legitimate message parameters.\n    /// </summary>\n    /// <returns>A unique message ID string.</returns>\n    public string GenerateMessageId()\n    {\n        const string chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n        char[] id = new char[8];\n\n        // Lock is required because Random is not thread-safe\n        lock (lockObject)\n        {\n            for (int i = 0; i < id.Length; i++)\n                id[i] = chars[random.Next(chars.Length)];\n        }\n\n        string messageId = \"MID_\" + new string(id);\n        Debug.Assert(IsValidMessageId(messageId), \"Invalid message ID generated.\");\n        return messageId;\n    }\n\n    public const int MESSAGE_ID_PREFIX_LENGTH = 4; // \"MID_\"\n    public const int MESSAGE_ID_LENGTH = 12; // \"MID_\" + 8 characters\n\n    /// <summary>\n    /// Checks if a string is a valid message ID.\n    /// Message IDs must start with \"MID_\" followed by 8 alphanumeric characters.\n    /// </summary>\n    /// <param name=\"value\">The string to validate.</param>\n    /// <returns>True if the string is a valid message ID, false otherwise.</returns>\n    public static bool IsValidMessageId(string value)\n    {\n        return !string.IsNullOrEmpty(value) &&\n               value.StartsWith(\"MID_\") &&\n               value.Length == MESSAGE_ID_LENGTH &&\n               value[MESSAGE_ID_PREFIX_LENGTH..].All(char.IsLetterOrDigit);\n    }\n\n    /// <summary>\n    /// Records a received message ID and determines if it's a duplicate.\n    /// Note: Uses DateTime.UtcNow for expiration timing. While a monotonic time source\n    /// would be more robust against system clock adjustments, DateTime is sufficient\n    /// for LAN lobby traffic where the 60-second expiration window is large.\n    /// </summary>\n    /// <param name=\"messageId\">The message ID to record.</param>\n    /// <param name=\"isDuplicate\">True if this message ID was already recorded (duplicate), false if it's new.</param>\n    public void AddMessage(string messageId, out bool isDuplicate)\n    {\n        if (string.IsNullOrEmpty(messageId))\n        {\n            // If no message ID provided, consider it not a duplicate\n            // This maintains backward compatibility with old clients\n            isDuplicate = false;\n            return;\n        }\n\n        DateTime expirationTime = DateTime.UtcNow.AddSeconds(messageIdExpirationSeconds);\n\n        // Try to add the message ID with expiration time in one atomic operation\n        // If it already exists, it's a duplicate\n        isDuplicate = !receivedMessageIds.TryAdd(messageId, expirationTime);\n    }\n\n    /// <summary>\n    /// Wraps a message payload with a message ID at the beginning.\n    /// </summary>\n    /// <param name=\"payload\">The original message payload.</param>\n    /// <returns>The wrapped message with message ID prepended.</returns>\n    public string WrapMessage(string payload)\n    {\n        string messageId = GenerateMessageId();\n        return messageId + payload;\n    }\n\n    /// <summary>\n    /// Unwraps a message, extracting the message ID from the beginning and returning the payload.\n    /// Also checks if the message is a duplicate.\n    /// </summary>\n    /// <param name=\"wrappedMessage\">The wrapped message with message ID at the beginning.</param>\n    /// <param name=\"payload\">The unwrapped message payload.</param>\n    /// <param name=\"isDuplicate\">True if this message ID was already recorded (duplicate), false if it's new.</param>\n    public void UnwrapMessage(string wrappedMessage, out string payload, out bool isDuplicate)\n    {\n        // Check if the message starts with a valid message ID\n        if (!string.IsNullOrEmpty(wrappedMessage) && wrappedMessage.Length >= MESSAGE_ID_LENGTH)\n        {\n            string potentialMessageId = wrappedMessage[..MESSAGE_ID_LENGTH];\n            if (IsValidMessageId(potentialMessageId))\n            {\n                // Extract message ID and payload\n                string messageId = potentialMessageId;\n                payload = wrappedMessage[MESSAGE_ID_LENGTH..];\n\n                // Check for duplicate\n                AddMessage(messageId, out isDuplicate);\n                return;\n            }\n        }\n\n        // No valid message ID found - treat as non-duplicate for backward compatibility\n        payload = wrappedMessage;\n        isDuplicate = false;\n    }\n\n    /// <summary>\n    /// Removes expired message IDs from the tracking dictionary.\n    /// This is called automatically by the background cleanup timer.\n    /// Note: This performs O(n) enumeration of all tracked IDs. For typical LAN lobby\n    /// traffic this is acceptable, but for high-traffic scenarios a more efficient\n    /// data structure (e.g., priority queue) could be considered.\n    /// ConcurrentDictionary operations (TryRemove, enumeration) are thread-safe.\n    /// </summary>\n    private void CleanupExpiredMessageIds()\n    {\n        // Check if disposed\n        if (disposed != 0)\n            return;\n\n        // Quick exit if there's nothing to clean up\n        if (receivedMessageIds.IsEmpty)\n            return;\n\n        DateTime now = DateTime.UtcNow;\n\n        // Find all expired message IDs\n        // ConcurrentDictionary enumeration is thread-safe\n        List<string> expiredIds = receivedMessageIds\n            .Where(kvp => kvp.Value < now)\n            .Select(kvp => kvp.Key)\n            .ToList();\n\n        // Remove expired IDs\n        // TryRemove is thread-safe\n        foreach (string id in expiredIds)\n            _ = receivedMessageIds.TryRemove(id, out _);\n    }\n\n    /// <summary>\n    /// Gets the current count of tracked message IDs.\n    /// Useful for monitoring and debugging.\n    /// </summary>\n    public int TrackedMessageCount => receivedMessageIds.Count;\n\n    /// <summary>\n    /// Clears all tracked message IDs.\n    /// </summary>\n    public void Clear()\n    {\n        receivedMessageIds.Clear();\n    }\n\n    /// <summary>\n    /// Disposes the message deduplicator and stops the cleanup timer.\n    /// </summary>\n    public void Dispose()\n    {\n        if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0)\n            cleanupTimer?.Dispose();\n\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/LANPlayerManager.cs",
    "content": "#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\n\nusing DTAClient.Domain.Multiplayer.LAN;\n\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer;\n\n/// <summary>\n/// Thread-safe manager for LAN lobby players.\n/// Encapsulates all player tracking operations to ensure atomicity between\n/// the player dictionary and UI updates.\n/// </summary>\ninternal class LANPlayerManager\n{\n    private readonly object lockObject = new();\n    private readonly Dictionary<string, LANLobbyUser> players = [];\n    private readonly Dictionary<string, int> usernameToListIndex = [];\n    private readonly XNAListBox playerListBox;\n\n    /// <summary>\n    /// Initializes a new instance of the LANPlayerManager class with the specified player list box.\n    /// \n    /// Note: after passing the XNAListBox, do not modify the XNAListBox directly! Use the methods of this class to ensure thread safety.\n    /// </summary>\n    /// <param name=\"playerListBox\">The XNAListBox control that displays the list of players in the LAN session. Cannot be null.</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if playerListBox is null.</exception>\n    public LANPlayerManager(XNAListBox playerListBox)\n    {\n        this.playerListBox = playerListBox ?? throw new ArgumentNullException(nameof(playerListBox));\n    }\n\n    private static string GetKeyFromEndPoint(IPEndPoint endPoint)\n        => endPoint.ToString();\n\n    /// <summary>\n    /// Gets or creates a player. Returns the LANLobbyUser instance (either newly created or existing).\n    /// This operation is atomic - both the internal dictionary and UI are updated together.\n    /// </summary>\n    /// <param name=\"endPoint\">The endpoint (IP:Port) that uniquely identifies this connection.</param>\n    /// <param name=\"name\">The player's username.</param>\n    /// <param name=\"gameTexture\">The game icon texture.</param>\n    /// <returns>The LANLobbyUser instance (either newly created or existing).</returns>\n    public LANLobbyUser GetOrCreatePlayer(IPEndPoint endPoint, string name, Texture2D gameTexture)\n    {\n        lock (lockObject)\n        {\n            string key = GetKeyFromEndPoint(endPoint);\n\n            // If this endpoint already exists, return the existing user\n            if (players.TryGetValue(key, out LANLobbyUser? existingUser))\n            {\n                return existingUser;\n            }\n\n            // Create new user\n            var newUser = new LANLobbyUser(name, gameTexture, endPoint);\n            players[key] = newUser;\n\n            // Add to UI if username not already displayed\n            if (!usernameToListIndex.ContainsKey(name))\n            {\n                // FIXME: This logic allows multiple players with the same username but different endpoints to exist simultaneously.\n                // Only the first player with a given username is shown in the UI.\n                // When that player disconnects, the username is removed from the UI even if other players with the same username are still connected.\n                // This can lead to invisible players.\n                // Consider either enforcing unique usernames or updating the UI tracking to handle multiple players per username correctly.\n\n                int index = playerListBox.Items.Count;\n                usernameToListIndex[name] = index;\n                playerListBox.AddItem(name, gameTexture);\n            }\n\n            return newUser;\n        }\n    }\n\n    /// <summary>\n    /// Attempts to get a player by endpoint.\n    /// </summary>\n    public LANLobbyUser? GetPlayerIfExist(IPEndPoint endPoint)\n    {\n        lock (lockObject)\n        {\n            string key = GetKeyFromEndPoint(endPoint);\n            _ = players.TryGetValue(key, out LANLobbyUser? user);\n            return user;\n        }\n    }\n\n    /// <summary>\n    /// Removes a player by endpoint. This operation is atomic.\n    /// </summary>\n    /// <returns>True if the player was removed, false if not found.</returns>\n    public bool RemovePlayer(IPEndPoint endPoint)\n    {\n        lock (lockObject)\n        {\n            string key = GetKeyFromEndPoint(endPoint);\n\n            if (!players.TryGetValue(key, out LANLobbyUser? user))\n                return false;\n\n            _ = players.Remove(key);\n\n            // Check if any other player has the same username\n            bool usernameStillInUse = players.Values.Any(p => p.Name == user.Name);\n\n            if (!usernameStillInUse && usernameToListIndex.TryGetValue(user.Name, out int index))\n            {\n                // Remove from UI\n                _ = usernameToListIndex.Remove(user.Name);\n                playerListBox.RemoveItem(index);\n\n                // Update indices for all usernames that came after the removed one\n                // We need to iterate carefully to avoid modifying the dictionary while iterating\n                List<string> keysToUpdate = usernameToListIndex\n                    .Where(kvp => kvp.Value > index)\n                    .Select(kvp => kvp.Key)\n                    .ToList();\n\n                // Apply the updates\n                foreach (string username in keysToUpdate)\n                    usernameToListIndex[username]--;\n            }\n\n            return true;\n        }\n    }\n\n    /// <summary>\n    /// Gets a thread-safe snapshot of all players.\n    /// </summary>\n    public List<LANLobbyUser> GetAllPlayers()\n    {\n        lock (lockObject)\n        {\n            return players.Values.ToList();\n        }\n    }\n\n    /// <summary>\n    /// Clears all players from both internal tracking and UI.\n    /// </summary>\n    public void Clear()\n    {\n        lock (lockObject)\n        {\n            players.Clear();\n            usernameToListIndex.Clear();\n            playerListBox.Clear();\n        }\n    }\n\n    /// <summary>\n    /// Gets the current player count.\n    /// </summary>\n    public int Count\n    {\n        get\n        {\n            lock (lockObject)\n            {\n                return players.Count;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    public class PlayerExtraOptionsPanel : XNAPanel\n    {\n        private const int maxStartCount = 8;\n        private const int defaultX = 24;\n        private const int defaultTeamStartMappingX = UIDesignConstants.EMPTY_SPACE_SIDES;\n        private const int teamMappingPanelWidth = 50;\n        private const int teamMappingPanelHeight = 22;\n        private readonly string customPresetName = \"Custom\".L10N(\"Client:Main:CustomPresetName\");\n\n        private XNAClientCheckBox chkBoxForceRandomSides;\n        private XNAClientCheckBox chkBoxForceNoTeams;\n        private XNAClientCheckBox chkBoxForceRandomColors;\n        private XNAClientCheckBox chkBoxForceRandomStarts;\n        private XNAClientCheckBox chkBoxUseTeamStartMappings;\n        private XNAClientDropDown ddTeamStartMappingPreset;\n        private TeamStartMappingsPanel teamStartMappingsPanel;\n        private bool _isHost;\n        private bool ignoreMappingChanges;\n\n        public EventHandler OptionsChanged;\n        public EventHandler OnClose;\n\n        private GameModeMap _gameModeMap;\n\n        public PlayerExtraOptionsPanel(WindowManager windowManager) : base(windowManager)\n        {\n        }\n\n        public bool ForcedRandomSides\n        {\n            get => chkBoxForceRandomSides.Checked;\n            set => chkBoxForceRandomSides.Checked = value;\n        }\n\n        public bool ForcedNoTeams\n        {\n            get => chkBoxForceNoTeams.Checked;\n            set => chkBoxForceNoTeams.Checked = value;\n        }\n\n        public bool ForcedNoTeamsAllowChecking\n        {\n            get => field;\n            set\n            {\n                field = value;\n                RefreshChkBoxForceNoTeams_AllowChecking();\n            }\n        }\n\n        public bool ForcedRandomColors\n        {\n            get => chkBoxForceRandomColors.Checked;\n            set => chkBoxForceRandomColors.Checked = value;\n        }\n\n        public bool ForcedRandomStarts\n        {\n            get => chkBoxForceRandomStarts.Checked;\n            set => chkBoxForceRandomStarts.Checked = value;\n        }\n\n        public bool UseTeamStartMappings\n        {\n            get => chkBoxUseTeamStartMappings.Checked;\n            set => chkBoxUseTeamStartMappings.Checked = value;\n        }\n\n        public bool UseTeamStartMappingsAllowChecking\n        {\n            get => chkBoxUseTeamStartMappings.AllowChecking;\n            set => chkBoxUseTeamStartMappings.AllowChecking = value;\n        }\n\n        private void Options_Changed(object sender, EventArgs e) => OptionsChanged?.Invoke(sender, e);\n\n        private void Mapping_Changed(object sender, EventArgs e)\n        {\n            Options_Changed(sender, e);\n            if (ignoreMappingChanges)\n                return;\n\n            ddTeamStartMappingPreset.SelectedIndex = 0;\n        }\n\n        private void ChkBoxUseTeamStartMappings_Changed(object sender, EventArgs e)\n        {\n            RefreshTeamStartMappingsPanel();\n            chkBoxForceNoTeams.Checked = chkBoxForceNoTeams.Checked || chkBoxUseTeamStartMappings.Checked;\n            RefreshChkBoxForceNoTeams_AllowChecking();\n\n            RefreshPresetDropdown();\n\n            Options_Changed(sender, e);\n        }\n\n        private void RefreshChkBoxForceNoTeams_AllowChecking()\n            => chkBoxForceNoTeams.AllowChecking = ForcedNoTeamsAllowChecking && !chkBoxUseTeamStartMappings.Checked;\n\n        private void RefreshTeamStartMappingsPanel()\n        {\n            teamStartMappingsPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked);\n\n            RefreshTeamStartMappingPanels();\n        }\n\n        private void AddLocationAssignments()\n        {\n            for (int i = 0; i < maxStartCount; i++)\n            {\n                var teamStartMappingPanel = new TeamStartMappingPanel(WindowManager, i + 1);\n                teamStartMappingPanel.ClientRectangle = GetTeamMappingPanelRectangle(i);\n\n                teamStartMappingsPanel.AddMappingPanel(teamStartMappingPanel);\n            }\n\n            teamStartMappingsPanel.MappingChanged += Mapping_Changed;\n        }\n\n        private Rectangle GetTeamMappingPanelRectangle(int index)\n        {\n            const int maxColumnCount = 2;\n            const int mappingPanelDefaultX = 4;\n            const int mappingPanelDefaultY = 0;\n            if (index > 0 && index % maxColumnCount == 0) // need to start a new column\n                return new Rectangle(((index / maxColumnCount) * (teamMappingPanelWidth + mappingPanelDefaultX)) + 3, mappingPanelDefaultY, teamMappingPanelWidth, teamMappingPanelHeight);\n\n            var lastControl = index > 0 ? teamStartMappingsPanel.GetTeamStartMappingPanels()[index - 1] : null;\n            return new Rectangle(lastControl?.X ?? mappingPanelDefaultX, lastControl?.Bottom + 4 ?? mappingPanelDefaultY, teamMappingPanelWidth, teamMappingPanelHeight);\n        }\n\n        private void ClearTeamStartMappingSelections()\n            => teamStartMappingsPanel.GetTeamStartMappingPanels().ForEach(panel => panel.ClearSelections());\n\n        private void RefreshTeamStartMappingPanels()\n        {\n            ClearTeamStartMappingSelections();\n            var teamStartMappingPanels = teamStartMappingsPanel.GetTeamStartMappingPanels();\n            for (int i = 0; i < teamStartMappingPanels.Count; i++)\n            {\n                var teamStartMappingPanel = teamStartMappingPanels[i];\n                teamStartMappingPanel.ClearSelections();\n                if (!UseTeamStartMappings)\n                    continue;\n\n                teamStartMappingPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked && _gameModeMap != null && _gameModeMap.AllowedStartingLocations.Contains(i + 1));\n                RefreshTeamStartMappingPresets(_gameModeMap?.Map?.TeamStartMappingPresets);\n            }\n        }\n\n        private void RefreshTeamStartMappingPresets(List<TeamStartMappingPreset> teamStartMappingPresets)\n        {\n            ddTeamStartMappingPreset.Items.Clear();\n            ddTeamStartMappingPreset.AddItem(new XNADropDownItem\n            {\n                Text = customPresetName,\n                Tag = new List<TeamStartMapping>()\n            });\n            ddTeamStartMappingPreset.SelectedIndex = 0;\n\n            if (!(teamStartMappingPresets?.Any() ?? false)) return;\n\n            teamStartMappingPresets.ForEach(preset => ddTeamStartMappingPreset.AddItem(new XNADropDownItem\n            {\n                Text = preset.Name,\n                Tag = preset.TeamStartMappings\n            }));\n            ddTeamStartMappingPreset.SelectedIndex = 1;\n        }\n\n        private void DdTeamMappingPreset_SelectedIndexChanged(object sender, EventArgs e)\n        {\n            var selectedItem = ddTeamStartMappingPreset.SelectedItem;\n            if (selectedItem?.Text == customPresetName)\n                return;\n\n            var teamStartMappings = selectedItem?.Tag as List<TeamStartMapping>;\n\n            ignoreMappingChanges = true;\n            teamStartMappingsPanel.SetTeamStartMappings(teamStartMappings);\n            ignoreMappingChanges = false;\n        }\n\n        private void RefreshPresetDropdown() => ddTeamStartMappingPreset.AllowDropDown = _isHost && chkBoxUseTeamStartMappings.Checked;\n\n        public override void Initialize()\n        {\n            Name = nameof(PlayerExtraOptionsPanel);\n            BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1);\n            Visible = false;\n\n            var btnClose = new XNAClientButton(WindowManager);\n            btnClose.Name = \"btnClose\";\n            btnClose.ClientRectangle = new Rectangle(0, 0, 0, 0);\n            btnClose.IdleTexture = AssetLoader.LoadTexture(\"optionsButtonClose.png\");\n            btnClose.HoverTexture = AssetLoader.LoadTexture(\"optionsButtonClose_c.png\");\n            btnClose.LeftClick += (sender, args) => Disable();\n            AddChild(btnClose);\n\n            var lblHeader = new XNALabel(WindowManager);\n            lblHeader.Name = \"lblHeader\";\n            lblHeader.Text = \"Extra Player Options\".L10N(\"Client:Main:ExtraPlayerOptions\");\n            lblHeader.ClientRectangle = new Rectangle(defaultX, 4, 0, 18);\n            AddChild(lblHeader);\n\n            chkBoxForceRandomSides = new XNAClientCheckBox(WindowManager);\n            chkBoxForceRandomSides.Name = \"chkBoxForceRandomSides\";\n            chkBoxForceRandomSides.Text = \"Force Random Sides\".L10N(\"Client:Main:ForceRandomSides\");\n            chkBoxForceRandomSides.ClientRectangle = new Rectangle(defaultX, lblHeader.Bottom + 4, 0, 0);\n            chkBoxForceRandomSides.CheckedChanged += Options_Changed;\n            AddChild(chkBoxForceRandomSides);\n\n            chkBoxForceRandomColors = new XNAClientCheckBox(WindowManager);\n            chkBoxForceRandomColors.Name = \"chkBoxForceRandomColors\";\n            chkBoxForceRandomColors.Text = \"Force Random Colors\".L10N(\"Client:Main:ForceRandomColors\");\n            chkBoxForceRandomColors.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomSides.Bottom + 4, 0, 0);\n            chkBoxForceRandomColors.CheckedChanged += Options_Changed;\n            AddChild(chkBoxForceRandomColors);\n\n            chkBoxForceNoTeams = new XNAClientCheckBox(WindowManager);\n            chkBoxForceNoTeams.Name = \"chkBoxForceNoTeams\";\n            chkBoxForceNoTeams.Text = \"Force No Teams\".L10N(\"Client:Main:ForceNoTeams\");\n            chkBoxForceNoTeams.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomColors.Bottom + 4, 0, 0);\n            chkBoxForceNoTeams.CheckedChanged += Options_Changed;\n            AddChild(chkBoxForceNoTeams);\n\n            chkBoxForceRandomStarts = new XNAClientCheckBox(WindowManager);\n            chkBoxForceRandomStarts.Name = \"chkBoxForceRandomStarts\";\n            chkBoxForceRandomStarts.Text = \"Force Random Starts\".L10N(\"Client:Main:ForceRandomStarts\");\n            chkBoxForceRandomStarts.ClientRectangle = new Rectangle(defaultX, chkBoxForceNoTeams.Bottom + 4, 0, 0);\n            chkBoxForceRandomStarts.CheckedChanged += Options_Changed;\n            AddChild(chkBoxForceRandomStarts);\n\n            /////////////////////////////\n\n            chkBoxUseTeamStartMappings = new XNAClientCheckBox(WindowManager);\n            chkBoxUseTeamStartMappings.Name = \"chkBoxUseTeamStartMappings\";\n            chkBoxUseTeamStartMappings.Text = \"Enable Auto Allying:\".L10N(\"Client:Main:EnableAutoAllying\");\n            chkBoxUseTeamStartMappings.ClientRectangle = new Rectangle(chkBoxForceRandomSides.X, chkBoxForceRandomStarts.Bottom + 20, 0, 0);\n            chkBoxUseTeamStartMappings.CheckedChanged += ChkBoxUseTeamStartMappings_Changed;\n            AddChild(chkBoxUseTeamStartMappings);\n\n            var btnHelp = new XNAClientButton(WindowManager);\n            btnHelp.Name = \"btnHelp\";\n            btnHelp.IdleTexture = AssetLoader.LoadTexture(\"questionMark.png\");\n            btnHelp.HoverTexture = AssetLoader.LoadTexture(\"questionMark_c.png\");\n            btnHelp.LeftClick += BtnHelp_LeftClick;\n            btnHelp.ClientRectangle = new Rectangle(chkBoxUseTeamStartMappings.Right + 4, chkBoxUseTeamStartMappings.Y - 1, 0, 0);\n            AddChild(btnHelp);\n\n            var lblPreset = new XNALabel(WindowManager);\n            lblPreset.Name = \"lblPreset\";\n            lblPreset.Text = \"Presets:\".L10N(\"Client:Main:Presets\");\n            lblPreset.ClientRectangle = new Rectangle(chkBoxUseTeamStartMappings.X, chkBoxUseTeamStartMappings.Bottom + 8, 0, 0);\n            AddChild(lblPreset);\n\n            ddTeamStartMappingPreset = new XNAClientDropDown(WindowManager);\n            ddTeamStartMappingPreset.Name = \"ddTeamStartMappingPreset\";\n            ddTeamStartMappingPreset.ClientRectangle = new Rectangle(lblPreset.X + 50, lblPreset.Y - 2, 160, 0);\n            ddTeamStartMappingPreset.SelectedIndexChanged += DdTeamMappingPreset_SelectedIndexChanged;\n            ddTeamStartMappingPreset.AllowDropDown = true;\n            AddChild(ddTeamStartMappingPreset);\n\n            teamStartMappingsPanel = new TeamStartMappingsPanel(WindowManager);\n            teamStartMappingsPanel.Name = \"teamStartMappingsPanel\";\n            teamStartMappingsPanel.ClientRectangle = new Rectangle(lblPreset.X, ddTeamStartMappingPreset.Bottom + 8, Width, Height - ddTeamStartMappingPreset.Bottom + 4);\n            AddChild(teamStartMappingsPanel);\n\n            AddLocationAssignments();\n\n            base.Initialize();\n\n            RefreshTeamStartMappingsPanel();\n        }\n\n        private void BtnHelp_LeftClick(object sender, EventArgs args)\n        {\n            XNAMessageBox.Show(WindowManager, \"Auto Allying\".L10N(\"Client:Main:AutoAllyingTitle\"),\n                (\"Auto allying allows the host to assign starting locations to teams, not players.\\n\" +\n                \"When players are assigned to spawn locations, they will be auto assigned to teams based on these mappings.\\n\" +\n                \"This is best used with random teams and random starts. However, only random teams is required.\\n\" +\n                \"Manually specified starts will take precedence.\").L10N(\"Client:Main:AutoAllyingText1\") + \"\\n\\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\" +\n                $\"{TeamStartMapping.NO_TEAM} : \" + \"Allow a player here, but don't assign a team.\".L10N(\"Client:Main:AutoAllyingTextNoTeamV2\")\n            );\n        }\n\n        public void UpdateForGameModeMap(GameModeMap gameModeMap)\n        {\n            if (_gameModeMap == gameModeMap)\n                return;\n\n            _gameModeMap = gameModeMap;\n\n            RefreshTeamStartMappingPanels();\n        }\n\n        public List<TeamStartMapping> GetTeamStartMappings()\n            => chkBoxUseTeamStartMappings.Checked ?\n                teamStartMappingsPanel.GetTeamStartMappings() : new List<TeamStartMapping>();\n\n        public void EnableControls(bool enable)\n        {\n            chkBoxForceRandomSides.InputEnabled = enable;\n            chkBoxForceRandomColors.InputEnabled = enable;\n            chkBoxForceRandomStarts.InputEnabled = enable;\n            chkBoxForceNoTeams.InputEnabled = enable;\n            chkBoxUseTeamStartMappings.InputEnabled = enable;\n\n            teamStartMappingsPanel.EnableControls(enable && chkBoxUseTeamStartMappings.Checked);\n        }\n\n        public PlayerExtraOptions GetPlayerExtraOptions()\n            => new PlayerExtraOptions()\n            {\n                IsForceRandomSides = ForcedRandomSides,\n                IsForceRandomColors = ForcedRandomColors,\n                IsForceRandomStarts = ForcedRandomStarts,\n                IsForceNoTeams = ForcedNoTeams,\n                IsUseTeamStartMappings = UseTeamStartMappings,\n                TeamStartMappings = GetTeamStartMappings()\n            };\n\n        public void SetPlayerExtraOptions(PlayerExtraOptions playerExtraOptions)\n        {\n            chkBoxForceRandomSides.Checked = playerExtraOptions.IsForceRandomSides;\n            chkBoxForceRandomColors.Checked = playerExtraOptions.IsForceRandomColors;\n            chkBoxForceNoTeams.Checked = playerExtraOptions.IsForceNoTeams;\n            chkBoxForceRandomStarts.Checked = playerExtraOptions.IsForceRandomStarts;\n            chkBoxUseTeamStartMappings.Checked = playerExtraOptions.IsUseTeamStartMappings;\n            teamStartMappingsPanel.SetTeamStartMappings(playerExtraOptions.TeamStartMappings);\n        }\n\n        public void SetIsHost(bool isHost)\n        {\n            _isHost = isHost;\n            RefreshPresetDropdown();\n            EnableControls(_isHost);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/PlayerListBox.cs",
    "content": "﻿using DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.Online;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing SixLabors.ImageSharp;\nusing Color = Microsoft.Xna.Framework.Color;\nusing Rectangle = Microsoft.Xna.Framework.Rectangle;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    /// <summary>\n    /// A list box for listing the players in the CnCNet lobby.\n    /// </summary>\n    public class PlayerListBox : XNAListBox\n    {\n        private const int MARGIN = 2;\n\n        public List<ChannelUser> Users;\n\n        private Texture2D adminGameIcon;\n        private Texture2D unknownGameIcon;\n        private Texture2D friendIcon;\n        private Texture2D ignoreIcon;\n        private Texture2D? voiceIcon;\n\n        private GameCollection gameCollection;\n\n        public PlayerListBox(WindowManager windowManager, GameCollection gameCollection) : base(windowManager)\n        {\n            this.gameCollection = gameCollection;\n\n            Users = new List<ChannelUser>();\n\n            var assembly = Assembly.GetAssembly(typeof(GameCollection));\n            using Stream cncnetIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.cncneticon.png\");\n            using Stream unknownIconStream = assembly.GetManifestResourceStream(\"DTAClient.Icons.unknownicon.png\");\n\n            adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream));\n            unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream));\n            friendIcon = AssetLoader.LoadTexture(\"friendicon.png\");\n            ignoreIcon = AssetLoader.LoadTexture(\"ignoreicon.png\");\n\n            const string voiceIconName = \"voiceicon.png\";\n            if (AssetLoader.AssetExists(voiceIconName))\n                voiceIcon = AssetLoader.LoadTexture(voiceIconName);\n        }\n\n        public void AddUser(ChannelUser user)\n        {\n            XNAListBoxItem item = new XNAListBoxItem();\n            UpdateItemInfo(user, item);\n            AddItem(item);\n        }\n\n        public void UpdateUserInfo(ChannelUser user)\n        {\n            XNAListBoxItem item = Items.Find(x => x.Tag == user);\n            UpdateItemInfo(user, item);\n        }\n\n        public override void Draw(GameTime gameTime)\n        {\n            DrawPanel();\n\n            int height = 2 - (ViewTop % LineHeight);\n\n            for (int i = TopIndex; i < Items.Count; i++)\n            {\n                XNAListBoxItem lbItem = Items[i];\n                var user = (ChannelUser)lbItem.Tag;\n\n                if (height > Height)\n                    break;\n\n                int x = TextBorderDistance;\n\n                if (i == SelectedIndex)\n                {\n                    int drawnWidth;\n\n                    if (DrawSelectionUnderScrollbar || !ScrollBar.IsDrawn() || !EnableScrollbar)\n                    {\n                        drawnWidth = Width - 2;\n                    }\n                    else\n                    {\n                        drawnWidth = Width - 2 - ScrollBar.Width;\n                    }\n\n                    FillRectangle(new Rectangle(1, height,\n                        drawnWidth, lbItem.TextLines.Count * LineHeight),\n                        FocusColor);\n                }\n\n                DrawTexture(user.IsAdmin ? adminGameIcon : lbItem.Texture, new Rectangle(x, height,\n                        adminGameIcon.Width, adminGameIcon.Height), Color.White);\n\n                x += adminGameIcon.Width + MARGIN;\n\n                // Friend Icon\n                if (user.IRCUser.IsFriend)\n                {\n                    DrawTexture(friendIcon,\n                        new Rectangle(x, height,\n                        friendIcon.Width, friendIcon.Height), Color.White);\n\n                    x += friendIcon.Width + MARGIN;\n                }\n                // Ignore Icon\n                else if (user.IRCUser.IsIgnored && !user.IsAdmin)\n                {\n                    DrawTexture(ignoreIcon,\n                        new Rectangle(x, height,\n                        ignoreIcon.Width, ignoreIcon.Height), Color.White);\n\n                    x += ignoreIcon.Width + MARGIN;\n                }\n\n                // Voice Icon\n                if (user.HasVoice && voiceIcon != null)\n                {\n                    DrawTexture(voiceIcon,\n                        new Rectangle(x, height, voiceIcon.Width, voiceIcon.Height), Color.White);\n\n                    x += voiceIcon.Width + MARGIN;\n                }\n\n                // Player Name\n                string name = user.IsAdmin ? user.IRCUser.Name + \" \" + \"(Admin)\".L10N(\"Client:Main:AdminSuffix\") : user.IRCUser.Name;\n                x += lbItem.TextXPadding;\n\n                DrawStringWithShadow(name, FontIndex,\n                    new Vector2(x, height),\n                    user.IsAdmin ? Color.Red : lbItem.TextColor);\n\n                height += LineHeight;\n            }\n\n            if (DrawBorders)\n                DrawPanelBorders();\n\n            DrawChildren(gameTime);\n        }\n\n        private void UpdateItemInfo(ChannelUser user, XNAListBoxItem item)\n        {\n            item.Tag = user;\n\n            if (user.IsAdmin)\n            {\n                item.Text = user.IRCUser.Name + \" \" + \"(Admin)\".L10N(\"Client:Main:AdminSuffix\");\n                item.TextColor = Color.Red;\n                item.Texture = adminGameIcon;\n            }\n            else\n            {\n                item.Text = user.IRCUser.Name;\n\n                if (user.IRCUser.GameID < 0 || user.IRCUser.GameID >= gameCollection.GameList.Count)\n                    item.Texture = unknownGameIcon;\n                else\n                    item.Texture = gameCollection.GameList[user.IRCUser.GameID].Texture;\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/TeamStartMappingPanel.cs",
    "content": "﻿using System;\nusing ClientGUI;\nusing DTAClient.Domain.Multiplayer;\nusing Microsoft.Xna.Framework;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    public class TeamStartMappingPanel : XNAPanel\n    {\n        private readonly int _start;\n        private readonly int _defaultTeamIndex = -1;\n\n        private const int ddWidth = 35;\n        // private XNAClientDropDown ddStarts;\n        private XNAClientDropDown ddTeams;\n\n        public event EventHandler OptionsChanged;\n\n        public TeamStartMappingPanel(WindowManager windowManager, int start) : base(windowManager)\n        {\n            _start = start;\n            DrawBorders = false;\n        }\n\n        public override void Initialize()\n        {\n            base.Initialize();\n\n            var startLabel = new XNALabel(WindowManager);\n            startLabel.Text = _start.ToString();\n            startLabel.ClientRectangle = new Rectangle(0, 0, 10, 22);\n            AddChild(startLabel);\n\n            ddTeams = new XNAClientDropDown(WindowManager);\n            ddTeams.Name = nameof(ddTeams);\n            ddTeams.ClientRectangle = new Rectangle(startLabel.Right, startLabel.Y - 3, ddWidth, 22);\n            TeamStartMapping.TEAMS.ForEach(ddTeams.AddItem);\n            AddChild(ddTeams);\n\n            ddTeams.SelectedIndexChanged += DD_SelectedItemChanged;\n        }\n\n        private void DD_SelectedItemChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(sender, e);\n\n        public void SetTeamStartMapping(TeamStartMapping teamStartMapping)\n        {\n            var teamIndex = teamStartMapping?.TeamIndex ?? _defaultTeamIndex;\n            \n            ddTeams.SelectedIndex = teamIndex >= 0 && teamIndex < ddTeams.Items.Count ? \n                teamIndex : -1;\n        }\n\n        public void EnableControls(bool enable) => ddTeams.AllowDropDown = enable;\n\n        public void ClearSelections() => ddTeams.SelectedIndex = _defaultTeamIndex;\n\n        public TeamStartMapping GetTeamStartMapping()\n        {\n            return new TeamStartMapping()\n            {\n                Team = ddTeams.SelectedItem?.Text,\n                Start = _start\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXGUI/Multiplayer/TeamStartMappingsPanel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing DTAClient.Domain.Multiplayer;\nusing Rampastring.XNAUI;\nusing Rampastring.XNAUI.XNAControls;\n\nnamespace DTAClient.DXGUI.Multiplayer\n{\n    public class TeamStartMappingsPanel : XNAPanel\n    {\n        public event EventHandler MappingChanged;\n\n        public TeamStartMappingsPanel(WindowManager windowManager) : base(windowManager)\n        {\n            DrawBorders = false;\n        }\n\n        public List<TeamStartMappingPanel> GetTeamStartMappingPanels() =>\n            Children.Select(c => c as TeamStartMappingPanel).ToList();\n\n        public void EnableControls(bool enable) =>\n            GetTeamStartMappingPanels().ForEach(panel => panel.EnableControls(enable));\n\n        public List<TeamStartMapping> GetTeamStartMappings()\n        {\n            return GetTeamStartMappingPanels()\n                .Select(panel => panel.GetTeamStartMapping())\n                .Where(mapping => mapping.IsValid)\n                .ToList();\n        }\n\n        public void AddMappingPanel(TeamStartMappingPanel teamStartMappingPanel)\n        {\n            teamStartMappingPanel.OptionsChanged += (sender, args) => MappingChanged?.Invoke(sender, args);\n            AddChild(teamStartMappingPanel);\n        }\n\n        public void SetTeamStartMappings(List<TeamStartMapping> teamStartMappings)\n        {\n            var teamStartMappingPanels = GetTeamStartMappingPanels();\n            for (int i = 0; i < teamStartMappingPanels.Count; i++)\n            {\n                if (teamStartMappings.Count <= i)\n                {\n                    teamStartMappingPanels[i].ClearSelections();\n                    continue;\n                }\n\n                var teamStartMapping = teamStartMappings[i];\n                teamStartMappingPanels[i].SetTeamStartMapping(teamStartMapping);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/DXMainClient.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType Condition=\"'$(Engine)' != 'UniversalGL'\">WinExe</OutputType>\n    <OutputType Condition=\"'$(Engine)' == 'UniversalGL'\">Exe</OutputType>\n    <!-- Specify Prefer32Bit only for .NET 4.8 XNA build. -->    \n    <Prefer32Bit Condition=\"'$(Engine)' == 'WindowsXNA' And '$(TargetFrameworkIdentifier)' == '.NETFramework'\">true</Prefer32Bit>\n    <Prefer32Bit Condition=\"'$(Engine)' != 'WindowsXNA' And '$(TargetFrameworkIdentifier)' == '.NETFramework'\">false</Prefer32Bit>\n    <!-- For .NET 8 builds, there are no Prefer32Bit option but RuntimeIdentifier. -->\n    <!-- Although it is not necessary to specify this RuntimeIdentifier parameter since we don't use AppHost, we still define it to make the `.deps.json` file correct. -->\n    <RuntimeIdentifier Condition=\"'$(Engine)' == 'WindowsXNA' And '$(TargetFrameworkIdentifier)' != '.NETFramework'\">win-x86</RuntimeIdentifier>\n    <UseCurrentRuntimeIdentifier>false</UseCurrentRuntimeIdentifier>\n    <UseAppHost>false</UseAppHost>\n    <SelfContained>false</SelfContained>\n    <Description>CnCNet Main Client</Description>\n    <AssemblyTitle>CnCNet Client</AssemblyTitle>\n    <RootNamespace>DTAClient</RootNamespace>\n    <ApplicationIcon>clienticon.ico</ApplicationIcon>\n    <ApplicationHighDpiMode Condition=\"'$(Engine)' == 'UniversalGL' OR '$(Engine)' == 'WindowsGL'\">SystemAware</ApplicationHighDpiMode>\n    <ApplicationHighDpiMode Condition=\"'$(Engine)' != 'UniversalGL' AND '$(Engine)' != 'WindowsGL'\">PerMonitorV2</ApplicationHighDpiMode>\n    <AssemblyName Condition=\"'$(Engine)' == 'WindowsDX'\">clientdx</AssemblyName>\n    <AssemblyName Condition=\"'$(Engine)' == 'UniversalGL' Or '$(Engine)' == 'WindowsGL'\">clientogl</AssemblyName>\n    <AssemblyName Condition=\"'$(Engine)' == 'WindowsXNA'\">clientxna</AssemblyName>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n  </PropertyGroup>\n  <PropertyGroup>\n    <!-- Suppress the warning on introducing a manifest file -->\n    <NoWarn>$(NoWarn);WFAC010;WFO0003</NoWarn>\n    <ApplicationManifest Condition=\"'$(Engine)' == 'UniversalGL' OR '$(Engine)' == 'WindowsGL'\">app.SystemAware.manifest</ApplicationManifest>\n    <ApplicationManifest Condition=\"'$(Engine)' != 'UniversalGL' AND '$(Engine)' != 'WindowsGL'\">app.PerMonitorV2.manifest</ApplicationManifest>\n  </PropertyGroup>\n  <ItemGroup>\n    <Compile Remove=\"Resources\\**\" />\n    <EmbeddedResource Remove=\"Resources\\**\" />\n    <None Remove=\"Resources\\**\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Remove=\"Icons\\*.png\" />\n    <EmbeddedResource Include=\"Icons\\*.png\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Content Include=\"clienticon.ico\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Facepunch.Steamworks\" GeneratePathProperty=\"true\" />\n    <PackageReference Include=\"SixLabors.ImageSharp\" />\n    <PackageReference Include=\"DiscordRichPresence\" />\n    <PackageReference Include=\"lzo.net\" />\n    <PackageReference Include=\"Microsoft.Extensions.Hosting\" />\n    <PackageReference Include=\"OpenMcdf\" />\n    <PackageReference Include=\"System.Management\" />\n    <PackageReference Include=\"System.DirectoryServices\" />\n    <PackageReference Include=\"System.Private.Uri\" />\n    <PackageReference Include=\"System.Text.Json\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ClientGUI\\ClientGUI.csproj\" />\n    <ProjectReference Include=\"..\\ClientUpdater\\ClientUpdater.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Include=\"$(PkgFacepunch_Steamworks)\\content\\steam_api64.dll\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "DXMainClient/Domain/CustomMissionHelper.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain;\ninternal static class CustomMissionHelper\n{\n    public static List<(string extension, string filename)>? CustomMissionSupplementDefinition { get; private set; }\n\n    private static bool IsValidExtension(string extension) => extension == extension.ToWin32FileName() && extension.IndexOfAny(new char[] { '.', ' ' }) == -1;\n\n    private static bool IsValidFileName(string filename) => filename == filename.ToWin32FileName();\n\n    public static void Initialize()\n    {\n        CustomMissionSupplementDefinition = GetCustomMissionSupplementDefinition();\n    }\n\n    public static List<(string extension, string filename)> GetCustomMissionSupplementDefinition()\n    {\n        List<(string extension, string copyAs)> configFiles = ClientConfiguration.Instance.GetCustomMissionSupplementFiles();\n        \n        HashSet<string> extensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        List<(string extension, string filename)> ret = [];\n\n        foreach ((string extension, string filename) in configFiles)\n        {\n            if (!IsValidExtension(extension))\n            {\n                throw new Exception(string.Format(\"Invalid extension {0}\", extension));\n            }\n\n            if (!IsValidFileName(filename))\n            {\n                throw new Exception(string.Format(\"Invalid file name {0}\", filename));\n            }\n\n            if (extensions.Contains(extension))\n            {\n                throw new Exception(string.Format(\"Extension {0} already exists\", extension));\n            }\n\n            extensions.Add(extension);\n\n            ret.Add((extension, filename));\n        }\n\n        return ret;\n    }\n\n    public static void DeleteSupplementalMissionFiles()\n    {\n        Debug.Assert(CustomMissionSupplementDefinition != null, \"CustomMissionHelper must be initialized.\");\n\n        IEnumerable<string> filenames = CustomMissionSupplementDefinition.Select(def => def.filename);\n        DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath);\n        foreach (string filename in filenames)\n        {\n            FileInfo? fileInfo = gameDirectory.EnumerateFiles(filename).SingleOrDefault();\n            if (fileInfo?.Exists ?? false)\n            {\n                fileInfo.IsReadOnly = false;\n                fileInfo.Delete();\n            }\n        }\n    }\n\n    public static void CopySupplementalMissionFiles(Mission mission)\n    {\n        Debug.Assert(CustomMissionSupplementDefinition != null, \"CustomMissionHelper must be initialized.\");\n\n        DeleteSupplementalMissionFiles();\n\n        if (mission.IsCustomMission)\n        {\n            string mapExtension = \".\" + ClientConfiguration.Instance.MapFileExtension; // e.g., \".map\"\n\n            string missionFileName = mission.Scenario;\n            Debug.Assert(missionFileName.EndsWith(mapExtension, StringComparison.InvariantCultureIgnoreCase), string.Format(\"Mission file should have the extension \\\"{0}\\\".\", mapExtension));\n\n            // copy the CSF file if exists\n            foreach ((string ext, string filename) in CustomMissionSupplementDefinition!)\n            {\n                string sourceFileName = missionFileName[..^mapExtension.Length] + \".\" + ext;\n                string sourceFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, sourceFileName);\n                if (SafePath.GetFile(sourceFilePath).Exists)\n                {\n                    string targetFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, filename);\n\n                    FileExtensions.CreateHardLinkFromSource(sourceFilePath, targetFilePath);\n                    new FileInfo(targetFilePath).IsReadOnly = true;\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/DirectDrawCompatibilityChecker.cs",
    "content": "#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.Versioning;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing ClientGUI;\n\nusing Microsoft.Win32;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.Domain;\n\n/// <summary>\n/// Handles checking and fixing DirectDraw compatibility issues with user interaction.\n/// </summary>\n[SupportedOSPlatform(\"windows\")]\npublic static class DirectDrawCompatibilityChecker\n{\n    private static readonly IReadOnlyList<string> OSCompatibilityValues = [\n        \"WIN8RTM\", \"WIN7RTM\", \"VISTASP2\", \"VISTASP1\", \"VISTARTM\", \"WINXPSP3\", \"WINXPSP2\", \"WIN98\", \"WIN95\"\n    ];\n\n    private static IEnumerable<string> GetExecutableFilePathsToCheck()\n    {\n        List<string> executablePaths = ClientConfiguration.Instance.GetCompatibilityCheckExecutables()\n            .Select(executableName => SafePath.CombineFilePath(ProgramConstants.GamePath, executableName))\n            .ToList();\n\n        // clientdx.exe, clientogl.exe, or clientxna.exe\n        string currentExePath = SafePath.GetFile(ProgramConstants.StartupExecutable).FullName;\n\n        executablePaths.Add(currentExePath);\n\n        Logger.Log(\"Checking compatibility settings for executables: \" +\n                   string.Join(\", \", executablePaths));\n\n        return executablePaths;\n    }\n\n    private static void Examine(out bool requireFix, out bool requireAdmin, out IEnumerable<string> problematicExeNames)\n    {\n        RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);\n        RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);\n\n        using RegistryKey? hkcuKey = hkcu.OpenSubKey(\n            @\"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\");\n        using RegistryKey? hklmKey = hklm.OpenSubKey(\n            @\"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\");\n\n        static bool IsFixRequired(object? regValue)\n            => regValue is string regValueString\n               && regValueString.Split([' ']).Intersect(OSCompatibilityValues).Any();\n\n        bool anyHkcuRequireFix = false;\n        bool anyHklmRequireFix = false;\n\n        var problematicExeNameHashSet = new HashSet<string>();\n        foreach (string exeFullPath in GetExecutableFilePathsToCheck())\n        {\n            object? hkcuValue = hkcuKey?.GetValue(exeFullPath);\n            object? hklmValue = hklmKey?.GetValue(exeFullPath);\n\n            if (IsFixRequired(hkcuValue))\n            {\n                Logger.Log($\"Executable '{exeFullPath}' has problematic compatibility settings in HKCU. Value: {hkcuValue}\");\n                anyHkcuRequireFix = true;\n                problematicExeNameHashSet.Add(Path.GetFileName(exeFullPath));\n            }\n\n            if (IsFixRequired(hklmValue))\n            {\n                Logger.Log($\"Executable '{exeFullPath}' has problematic compatibility settings in HKLM. Value: {hklmValue}\");\n                anyHklmRequireFix = true;\n                problematicExeNameHashSet.Add(Path.GetFileName(exeFullPath));\n            }\n        }\n\n        requireFix = anyHkcuRequireFix || anyHklmRequireFix;\n        requireAdmin = anyHklmRequireFix;\n        problematicExeNames = problematicExeNameHashSet;\n    }\n\n    private static string FixCompatLayerString(string value) => string.Join(\" \",\n            value\n                .SplitWithCleanup(new[] { ' ' })\n                .Where(v => !OSCompatibilityValues.Contains(v, StringComparer.InvariantCultureIgnoreCase)));\n\n    private static void Fix()\n    {\n        void FixRegValue(object? regValue, out bool success, out string newRegValue)\n        {\n            if (regValue is string regValueString)\n            {\n                newRegValue = FixCompatLayerString(regValueString);\n                success = true;\n            }\n            else\n            {\n                success = false;\n                newRegValue = string.Empty;\n            }\n        }\n\n        void FixRegistryKey(RegistryKey rootKey, string subKeyPath)\n        {\n            try\n            {\n                using RegistryKey? key = rootKey.OpenSubKey(subKeyPath, writable: true);\n                if (key == null)\n                    return;\n\n                foreach (string exeFullPath in GetExecutableFilePathsToCheck())\n                {\n                    object? value = key.GetValue(exeFullPath);\n\n                    FixRegValue(value, out bool success, out string newValue);\n\n                    if (success)\n                    {\n                        if (string.IsNullOrEmpty(newValue))\n                            key.DeleteValue(exeFullPath, false);\n                        else\n                            key.SetValue(exeFullPath, newValue, RegistryValueKind.String);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"Failed to fix registry key {rootKey.Name}\\\\{subKeyPath}: {ex.Message}\");\n            }\n        }\n\n        string subKeyPath = @\"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers\";\n\n        RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);\n        RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);\n\n        FixRegistryKey(hkcu, subKeyPath);\n\n        FixRegistryKey(hklm, subKeyPath);\n    }\n\n    /// <summary>\n    /// Checks for DirectDraw compatibility issues and prompts the user to fix them.\n    /// </summary>\n    /// <param name=\"windowManager\">The WindowManager for displaying message boxes.</param>\n    public static void CheckAndPromptFix(WindowManager windowManager)\n    {\n        // Fix environment variable __COMPAT_LAYER first, for the client itself.\n        string compatLayerEnv = Environment.GetEnvironmentVariable(\"__COMPAT_LAYER\") ?? string.Empty;\n        string fixedCompatLayerEnv = FixCompatLayerString(compatLayerEnv);\n        if (compatLayerEnv != fixedCompatLayerEnv)\n        {\n            Logger.Log(\"Fixing __COMPAT_LAYER environment variable. Previous value: \" +\n                       $\"'{compatLayerEnv}', new value: '{fixedCompatLayerEnv}'\");\n            Environment.SetEnvironmentVariable(\"__COMPAT_LAYER\", fixedCompatLayerEnv);\n        }\n\n        // Now check registry compatibility settings for all relevant executables.\n        try\n        {\n            Examine(out bool requireFix, out bool requireAdmin, out var problematicExeNames);\n\n            if (!requireFix)\n                return;\n\n            Logger.Log(\"DirectDraw compatibility issue detected.\");\n\n            string localizedMessage = \"Problematic Windows compatibility mode settings have been detected that may interfere with the game.\"\n                .L10N(\"Client:Main:ProblematicCompatibilityText1\") + \"\\n\\n\"\n                + \"Affected executables:\".L10N(\"Client:Main:ProblematicCompatibilityText2\")\n                + \"\\n- \" + string.Join(\"\\n- \", problematicExeNames) + \"\\n\\n\" +\n                \"Would you like to remove these compatibility settings now?\".L10N(\"Client:Main:ProblematicCompatibilityText3\");\n\n            if (requireAdmin && !AdminRestarter.IsRunningAsAdministrator())\n            {\n                localizedMessage += \"\\n\\n\" + (\"Note: Administrator privileges are required to remove compatibility settings.\" + \" \" +\n                    \"Clicking Yes will relaunch the client with administrator permissions.\").L10N(\"Client:Main:ProblematicCompatibilityText4\");\n            }\n\n            var messageBox = XNAMessageBox.ShowYesNoDialog(windowManager,\n                \"Problematic Compatibility Settings Detected\".L10N(\"Client:Main:ProblematicCompatibilityTitle\"),\n                localizedMessage);\n\n            messageBox.YesClickedAction = _ =>\n            {\n                if (requireAdmin && !AdminRestarter.IsRunningAsAdministrator())\n                {\n                    Logger.Log(\"Administrator privileges required. Restart with elevated privileges.\");\n\n                    if (AdminRestarter.RestartAsAdmin())\n                        windowManager.CloseGame();\n                }\n                else\n                {\n                    Logger.Log(\"Attempting to fix DirectDraw compatibility settings.\");\n                    Fix();\n                    Logger.Log(\"DirectDraw compatibility settings fixed successfully.\");\n\n                    XNAMessageBox.Show(windowManager,\n                        \"Fix Applied\".L10N(\"Client:Main:CompatibilityFixAppliedTitle\"),\n                        \"Compatibility settings have been removed successfully.\".L10N(\"Client:Main:CompatibilityFixAppliedText\"));\n                }\n            };\n\n            messageBox.NoClickedAction = _ =>\n            {\n                Logger.Log(\"User declined to fix DirectDraw compatibility settings.\");\n            };\n        }\n        catch (Exception ex)\n        {\n            Logger.Log(\"Error checking DirectDraw compatibility: \" + ex.ToString());\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/DirectDrawWrapper.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nnamespace DTAClient.Domain\n{\n    /// <summary>\n    /// A DirectDraw wrapper option.\n    /// </summary>\n    public class DirectDrawWrapper\n    {\n        /// <summary>\n        /// Creates a new DirectDrawWrapper instance and parses its configuration\n        /// from an INI file.\n        /// </summary>\n        /// <param name=\"internalName\">The internal name of the renderer.</param>\n        /// <param name=\"iniFile\">The file to parse the renderer's options from.</param>\n        public DirectDrawWrapper(string internalName, IniFile iniFile)\n        {\n            InternalName = internalName;\n            Parse(iniFile.GetSection(InternalName));\n        }\n\n        public string InternalName { get; private set; }\n        public string UIName { get; private set; }\n\n        /// <summary>\n        /// If not null or empty, windowed mode will be written to an INI key\n        /// in this section of the renderer settings file instead\n        /// of the regular game settings INI file.\n        /// </summary>\n        public string WindowedModeSection { get; private set; }\n\n        /// <summary>\n        /// If not null or empty, windowed mode will be written to this INI key\n        /// in the section defined in <see cref=\"DirectDrawWrapper.WindowedModeSection\"/> \n        /// instead of the regular settings INI file.\n        /// </summary>\n        public string WindowedModeKey { get; private set; }\n\n        /// <summary>\n        /// If not null or empty, the setting that controls whether the game is \n        /// run in borderless windowed mode will be written to this INI key in\n        /// the section defined by\n        /// <see cref=\"DirectDrawWrapper.WindowedModeSection\"/> instead of the\n        /// regular settings INI file.\n        /// </summary>\n        public string BorderlessWindowedModeKey { get; private set; }\n\n        /// <summary>\n        /// If set, borderless mode is enabled if the setting is \"false\"\n        /// and disabled if the setting is \"true\".\n        /// </summary>\n        public bool IsBorderlessWindowedModeKeyReversed { get; private set; }\n\n        public bool Hidden { get; private set; }\n\n        /// <summary>\n        /// Many ddraw wrappers need qres.dat to set the desktop to 16 bit mode\n        /// </summary>\n        public bool UseQres { get; private set; } = true;\n\n        /// <summary>\n        /// If set to false, the client won't set single-core affinity\n        /// to the game executable when this renderer is used.\n        /// </summary>\n        public bool SingleCoreAffinity { get; private set; } = true;\n\n        /// <summary>\n        /// The filename of the configuration INI of the renderer in the game directory.\n        /// </summary>\n        public string ConfigFileName { get; private set; }\n\n        /// <summary>\n        /// Indicates whether this DirectDrawWrapper is a dummy wrapper (i.e. no wrapper).\n        /// </summary>\n        public bool IsDummy => string.IsNullOrEmpty(ddrawDLLPath);\n\n        private string ddrawDLLPath;\n        private string resConfigFileName;\n        private List<string> filesToCopy = new List<string>();\n        private List<OSVersion> disallowedOSList = new List<OSVersion>();\n\n        /// <summary>\n        /// Reads the properties of this DirectDrawWrapper from an INI section.\n        /// </summary>\n        /// <param name=\"section\">The INI section.</param>\n        private void Parse(IniSection section)\n        {\n            if (section == null)\n            {\n                Logger.Log(\"DirectDrawWrapper: Configuration for renderer '\" + InternalName + \"' not found!\");\n                return;\n            }\n\n            UIName = section.GetStringValue(\"UIName\", \"Unnamed renderer\");\n\n            if (section.GetBooleanValue(\"IsDxWnd\", false))\n            {\n                // For backwards compatibility with previous client versions\n                WindowedModeSection = \"DxWnd\";\n                WindowedModeKey = \"RunInWindow\";\n                BorderlessWindowedModeKey = \"NoWindowFrame\";\n            }\n\n            WindowedModeSection = section.GetStringValue(\"WindowedModeSection\", WindowedModeSection);\n            WindowedModeKey = section.GetStringValue(\"WindowedModeKey\", WindowedModeKey);\n            BorderlessWindowedModeKey = section.GetStringValue(\"BorderlessWindowedModeKey\", BorderlessWindowedModeKey);\n            IsBorderlessWindowedModeKeyReversed = section.GetBooleanValue(\"IsBorderlessWindowedModeKeyReversed\",\n                IsBorderlessWindowedModeKeyReversed);\n\n            if (BorderlessWindowedModeKey != null && WindowedModeSection == null)\n            {\n                throw new DirectDrawWrapperConfigurationException(\n                    \"BorderlessWindowedModeKey= is defined for renderer\" +\n                    $\" {InternalName} but WindowedModeSection= is not!\");\n            }\n\n            Hidden = section.GetBooleanValue(\"Hidden\", false);\n            UseQres = section.GetBooleanValue(\"UseQres\", UseQres);\n            SingleCoreAffinity = section.GetBooleanValue(\"SingleCoreAffinity\", SingleCoreAffinity);\n            ddrawDLLPath = section.GetStringValue(\"DLLName\", string.Empty);\n            ConfigFileName = section.GetStringValue(\"ConfigFileName\", string.Empty);\n            resConfigFileName = section.GetStringValue(\"ResConfigFileName\", ConfigFileName);\n\n            filesToCopy = section.GetStringValue(\"AdditionalFiles\", string.Empty).Split(\n                new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();\n\n            string[] disallowedOSs = section.GetStringValue(\"DisallowedOperatingSystems\", string.Empty).Split(\n                new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);\n\n            foreach (string os in disallowedOSs)\n            {\n                OSVersion disallowedOS = (OSVersion)Enum.Parse(typeof(OSVersion), os.Trim());\n                disallowedOSList.Add(disallowedOS);\n            }\n\n            if (!string.IsNullOrEmpty(ddrawDLLPath) &&\n                !SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath).Exists)\n                Logger.Log(\"DirectDrawWrapper: File specified in DLLPath= for renderer '\" + InternalName + \"' does not exist!\");\n\n            if (!string.IsNullOrEmpty(resConfigFileName) &&\n                !SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), resConfigFileName).Exists)\n                Logger.Log(\"DirectDrawWrapper: File specified in ConfigFileName= for renderer '\" + InternalName + \"' does not exist!\");\n\n            foreach (var file in filesToCopy)\n            {\n                if (!SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), file).Exists)\n                    Logger.Log(\"DirectDrawWrapper: Additional file '\" + file + \"' for renderer '\" + InternalName + \"' does not exist!\");\n            }\n        }\n\n        /// <summary>\n        /// Returns true if this wrapper is compatible with the given operating\n        /// system, otherwise false.\n        /// </summary>\n        /// <param name=\"os\">The operating system.</param>\n        public bool IsCompatibleWithOS(OSVersion os)\n        {\n            return !disallowedOSList.Contains(os);\n        }\n\n        /// <summary>\n        /// Applies the renderer's files to the game directory.\n        /// </summary>\n        public void Apply()\n        {\n            string ddrawDllSourcePath = SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath);\n            string ddrawDllTargetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, \"ddraw.dll\");\n\n            if (!string.IsNullOrEmpty(ddrawDLLPath))\n            {\n                FileExtensions.CreateHardLinkFromSource(ddrawDllSourcePath, ddrawDllTargetPath);\n                new FileInfo(ddrawDllSourcePath).IsReadOnly = true;\n                new FileInfo(ddrawDllTargetPath).IsReadOnly = true;\n            }\n            else\n            {\n                if (File.Exists(ddrawDllTargetPath))\n                {\n                    new FileInfo(ddrawDllTargetPath).IsReadOnly = false;\n                    File.Delete(ddrawDllTargetPath);\n                }\n            }\n\n\n            if (!string.IsNullOrEmpty(ConfigFileName) && !string.IsNullOrEmpty(resConfigFileName)\n                && !SafePath.GetFile(ProgramConstants.GamePath, ConfigFileName).Exists) // Do not overwrite settings\n            {\n                File.Copy(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), resConfigFileName), SafePath.CombineFilePath(ProgramConstants.GamePath, Path.GetFileName(ConfigFileName)));\n            }\n\n            foreach (var file in filesToCopy)\n            {\n                File.Copy(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), file), SafePath.CombineFilePath(ProgramConstants.GamePath, Path.GetFileName(file)), true);\n            }\n        }\n\n        /// <summary>\n        /// Call to clean the renderer's files from the game directory.\n        /// </summary>\n        public void Clean()\n        {\n            if (!string.IsNullOrEmpty(ConfigFileName))\n                SafePath.DeleteFileIfExists(ProgramConstants.GamePath, Path.GetFileName(ConfigFileName));\n\n            foreach (var file in filesToCopy)\n                SafePath.DeleteFileIfExists(ProgramConstants.GamePath, Path.GetFileName(file));\n        }\n\n        /// <summary>\n        /// Checks whether this renderer enables windowed mode through its\n        /// own configuration INI file instead of the game settings INI file.\n        /// </summary>\n        public bool UsesCustomWindowedOption()\n        {\n            return !string.IsNullOrEmpty(WindowedModeSection) &&\n                !string.IsNullOrEmpty(WindowedModeKey);\n        }\n\n        // Override == and != operators to compare by InternalName\n        public static bool operator ==(DirectDrawWrapper a, DirectDrawWrapper b)\n        {\n            if (ReferenceEquals(a, b))\n                return true;\n\n            if (a is null || b is null)\n                return false;\n\n            return a.InternalName == b.InternalName;\n        }\n\n        public static bool operator !=(DirectDrawWrapper a, DirectDrawWrapper b) => !(a == b);\n\n        public override bool Equals(object obj)\n        {\n            if (obj is DirectDrawWrapper other)\n            {\n                return this == other;\n            }\n            return false;\n        }\n\n        public override int GetHashCode() => InternalName.GetHashCode();\n    }\n\n    /// <summary>\n    /// An exception that is thrown when configuration for DirectDraw wrapper contains\n    /// invalid or unexpected settings / data or required settings / data are missing.\n    /// </summary>\n    class DirectDrawWrapperConfigurationException : Exception\n    {\n        public DirectDrawWrapperConfigurationException(string message) : base(message)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/DirectDrawWrapperManager.cs",
    "content": "﻿#nullable enable\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\n\nusing ClientGUI;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain\n{\n    public class DirectDrawWrapperManager\n    {\n        private const string RENDERERS_INI = \"Renderers.ini\";\n        private List<DirectDrawWrapper> renderers;\n\n        private string defaultRenderer;\n        private DirectDrawWrapper selectedRenderer;\n        public DirectDrawWrapper SelectedRenderer => selectedRenderer;\n\n#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor\n        public DirectDrawWrapperManager()\n        {\n            // This method sets up `renderers`, `defaultRenderer`, and `selectedRenderer`\n            RefreshRenderers();\n        }\n#pragma warning restore CS8618\n\n        public IEnumerable<DirectDrawWrapper> GetRenderers(OSVersion localOS)\n            => renderers.Where(r => r.IsCompatibleWithOS(localOS) && !r.Hidden);\n\n        private void RefreshRenderers()\n        {\n            renderers = new List<DirectDrawWrapper>();\n\n            var renderersIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), RENDERERS_INI));\n\n            var keys = renderersIni.GetSectionKeys(\"Renderers\");\n            if (keys == null)\n                throw new ClientConfigurationException(\"[Renderers] not found from Renderers.ini!\");\n\n            foreach (string key in keys)\n            {\n                string internalName = renderersIni.GetStringValue(\"Renderers\", key, string.Empty);\n\n                var ddWrapper = new DirectDrawWrapper(internalName, renderersIni);\n                renderers.Add(ddWrapper);\n            }\n\n            OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion();\n\n            defaultRenderer = renderersIni.GetStringValue(\"DefaultRenderer\", osVersion.ToString(), string.Empty);\n\n            if (string.IsNullOrEmpty(defaultRenderer))\n                throw new ClientConfigurationException(\"Invalid or missing default renderer for operating system: \" + osVersion);\n\n            string renderer = UserINISettings.Instance.Renderer;\n\n            selectedRenderer = renderers.Find(r => r.InternalName == renderer)\n                ?? renderers.Find(r => r.InternalName == defaultRenderer)\n                ?? throw new ClientConfigurationException(\"Missing renderer: \" + renderer);\n\n            GameProcessLogic.UseQres = selectedRenderer.UseQres;\n            GameProcessLogic.SingleCoreAffinity = selectedRenderer.SingleCoreAffinity;\n        }\n\n        public void Save(DirectDrawWrapper? newSelectedRenderer)\n        {\n            var originalRenderer = selectedRenderer;\n            selectedRenderer = newSelectedRenderer ?? originalRenderer;\n\n            if (selectedRenderer != originalRenderer ||\n                !SafePath.GetFile(ProgramConstants.GamePath, selectedRenderer.ConfigFileName).Exists)\n            {\n                foreach (var renderer in renderers.Where(renderer => renderer != selectedRenderer))\n                {\n                    renderer.Clean();\n                }\n            }\n\n            selectedRenderer.Apply();\n\n            GameProcessLogic.UseQres = selectedRenderer.UseQres;\n            GameProcessLogic.SingleCoreAffinity = selectedRenderer.SingleCoreAffinity;\n\n            UserINISettings.Instance.Renderer.Value = selectedRenderer.InternalName;\n        }\n\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/DiscordHandler.cs",
    "content": "﻿using System;\nusing ClientCore;\nusing DiscordRPC;\nusing DiscordRPC.Message;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System.Text.RegularExpressions;\n\nnamespace DTAClient.Domain\n{\n    /// <summary>\n    /// A class for handling Discord integration.\n    /// </summary>\n    public class DiscordHandler: IDisposable\n    {\n        private const int MaxDiscordPresenceTextLength = 128;\n        private DiscordRpcClient client;\n\n        private RichPresence _currentPresence;\n\n        /// <summary>\n        /// RichPresence instance that is currently being displayed.\n        /// </summary>\n        public RichPresence CurrentPresence\n        {\n            get\n            {\n                return _currentPresence;\n            }\n            set\n            {\n                if (_currentPresence == null || !_currentPresence.Equals(PreviousPresence))\n                {\n                    PreviousPresence = _currentPresence;\n                    _currentPresence = value;\n                    client?.SetPresence(_currentPresence);\n                }\n            }\n        }\n\n        /// <summary>\n        /// RichPresence instance that was last displayed before the current one.\n        /// </summary>\n        public RichPresence PreviousPresence { get; private set; }\n\n        /// <summary>\n        /// Creates a new instance of Discord handler.\n        /// </summary>\n        public DiscordHandler()\n        {\n            if (!UserINISettings.Instance.DiscordIntegration || ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled)\n                return;\n\n            InitializeClient();\n            UpdatePresence();\n            Connect();\n\n            AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();\n        }\n\n        #region overrides\n\n        #endregion\n\n        #region methods\n\n        /// <summary>\n        /// Initializes or reinitializes Discord RPC client object & event handlers.\n        /// </summary>\n        private void InitializeClient()\n        {\n            if (client != null && client.IsInitialized)\n            {\n                client.ClearPresence();\n                client.Dispose();\n                client = null;\n            }\n\n            client = new DiscordRpcClient(ClientConfiguration.Instance.DiscordAppId);\n            client.OnReady += OnReady;\n            client.OnClose += OnClose;\n            client.OnError += OnError;\n            client.OnConnectionEstablished += OnConnectionEstablished;\n            client.OnConnectionFailed += OnConnectionFailed;\n            client.OnPresenceUpdate += OnPresenceUpdate;\n            client.OnSubscribe += OnSubscribe;\n            client.OnUnsubscribe += OnUnsubscribe;\n\n            if (CurrentPresence != null)\n                client.SetPresence(CurrentPresence);\n        }\n\n        /// <summary>\n        /// Connects to Discord.\n        /// Does not do anything if the Discord RPC client has not been initialized or is already connected.\n        /// </summary>\n        public void Connect()\n        {\n            if (client == null || client.IsInitialized)\n                return;\n\n            bool success = client.Initialize();\n\n            if (success)\n                Logger.Log(\"DiscordHandler: Connected Discord RPC client.\");\n            else\n                Logger.Log(\"DiscordHandler: Failed to connect Discord RPC client.\");\n        }\n\n        /// <summary>\n        /// Disconnects from Discord.\n        /// Does not do anything if the Discord RPC client has not been initialized or is not connected.\n        /// </summary>\n        public void Disconnect()\n        {\n            if (client == null || !client.IsInitialized)\n                return;\n\n            // HACK warning\n            // Currently DiscordRpcClient does not appear to have any way to reliably disconnect and reconnect using same client object.\n            // Deinitialize does not appear to completely reset connection state & resources and any attempts to call Initialize afterwards will fail.\n            // A hacky solution is to dispose current client object and create and initialize a new one.\n            InitializeClient(); //client.Deinitialize();\n\n            Logger.Log(\"DiscordHandler: Disconnected Discord RPC client.\");\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with default info.\n        /// </summary>\n        public void UpdatePresence()\n        {\n            CurrentPresence = new RichPresence()\n            {\n                Details = \"In Client\",\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\"\n                }\n            };\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with info from game lobbies.\n        /// </summary>\n        public void UpdatePresence(string map, string mode, string type, string state,\n            int players, int maxPlayers, string side, string roomName,\n            bool isHost = false, bool isPassworded = false,\n            bool isLocked = false, bool resetTimer = false)\n        {\n            string sideKey = new Regex(\"[^a-zA-Z0-9]\").Replace(side.ToLower(), \"\");\n            string stateString = $\"{state} [{players}/{maxPlayers}] • {roomName}\";\n            if (isHost)\n                stateString += \"👑\";\n            if (isPassworded)\n                stateString += \"🔑\";\n            if (isLocked)\n                stateString += \"🔒\";\n            CurrentPresence = new RichPresence()\n            {\n                State = TrimDiscordPresenceText(stateString),\n                Details = TrimDiscordPresenceText($\"{type} • {map} • {mode}\"),\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\",\n                    SmallImageKey = sideKey,\n                    SmallImageText = TrimDiscordPresenceText(side)\n                },\n                Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ?\n                    client.CurrentPresence.Timestamps : Timestamps.Now\n            };\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with info from game loading lobbies.\n        /// </summary>\n        public void UpdatePresence(string map, string mode, string type, string state,\n            int players, int maxPlayers, string roomName,\n            bool isHost = false, bool resetTimer = false)\n        {\n            string stateString = $\"{state} [{players}/{maxPlayers}] • {roomName}\";\n            stateString += \"💾\";\n            if (isHost)\n                stateString += \"👑\";\n            CurrentPresence = new RichPresence()\n            {\n                State = TrimDiscordPresenceText(stateString),\n                Details = TrimDiscordPresenceText($\"{type} • {map} • {mode}\"),\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\"\n                },\n                Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ?\n                    client.CurrentPresence.Timestamps : Timestamps.Now\n            };\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with info from skirmish \"lobby\".\n        /// </summary>\n        public void UpdatePresence(string map, string mode, string state, string side, bool resetTimer = false)\n        {\n            string sideKey = new Regex(\"[^a-zA-Z0-9]\").Replace(side.ToLower(), \"\");\n            CurrentPresence = new RichPresence()\n            {\n                State = TrimDiscordPresenceText(state),\n                Details = TrimDiscordPresenceText($\"Skirmish • {map} • {mode}\"),\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\",\n                    SmallImageKey = sideKey,\n                    SmallImageText = TrimDiscordPresenceText(side)\n                },\n                Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ?\n                    client.CurrentPresence.Timestamps : Timestamps.Now\n            };\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with info from campaign screen.\n        /// </summary>\n        public void UpdatePresence(string mission, string difficulty, string side, bool resetTimer = false)\n        {\n            string sideKey = new Regex(\"[^a-zA-Z0-9]\").Replace(side.ToLower(), \"\");\n            CurrentPresence = new RichPresence()\n            {\n                State = \"Playing Mission\",\n                Details = TrimDiscordPresenceText($\"{mission} • {difficulty}\"),\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\",\n                    SmallImageKey = sideKey,\n                    SmallImageText = TrimDiscordPresenceText(side)\n                },\n                Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ?\n                    client.CurrentPresence.Timestamps : Timestamps.Now\n            };\n        }\n\n        /// <summary>\n        /// Updates Discord Rich Presence with info from game loading screen.\n        /// </summary>\n        public void UpdatePresence(string save, bool resetTimer = false)\n        {\n            CurrentPresence = new RichPresence()\n            {\n                State = \"Playing Saved Game\",\n                Details = TrimDiscordPresenceText(save),\n                Assets = new Assets()\n                {\n                    LargeImageKey = \"logo\"\n                },\n                Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ?\n                    client.CurrentPresence.Timestamps : Timestamps.Now\n            };\n        }\n\n        private static string TrimDiscordPresenceText(string value)\n        {\n            if (string.IsNullOrEmpty(value) || value.Length <= MaxDiscordPresenceTextLength)\n                return value;\n\n            return value.Substring(0, MaxDiscordPresenceTextLength - 3) + \"...\";\n        }\n\n        #endregion\n\n        #region eventhandlers\n\n        private void OnReady(object sender, ReadyMessage args)\n        {\n            Logger.Log($\"Discord: Received Ready from user {args.User.Username}\");\n            client?.SetPresence(CurrentPresence);\n        }\n\n        private void OnClose(object sender, CloseMessage args)\n        {\n            Logger.Log($\"Discord: Lost Connection with client because of '{args.Reason}'\");\n        }\n\n        private void OnError(object sender, ErrorMessage args)\n        {\n            Logger.Log($\"Discord: Error occured. ({args.Code}) {args.Message}\");\n        }\n\n        private void OnConnectionEstablished(object sender, ConnectionEstablishedMessage args)\n        {\n            Logger.Log($\"Discord: Pipe Connection Established. Valid on pipe #{args.ConnectedPipe}\");\n        }\n\n        private void OnConnectionFailed(object sender, ConnectionFailedMessage args)\n        {\n            Logger.Log($\"Discord: Pipe Connection Failed. Could not connect to pipe #{args.FailedPipe}\");\n        }\n\n        private void OnPresenceUpdate(object sender, PresenceMessage args)\n        {\n            Logger.Log($\"Discord: Rich Presence Updated. State: {args.Presence?.State}; Details: {args.Presence?.Details}\");\n        }\n\n        private void OnSubscribe(object sender, SubscribeMessage args)\n        {\n            Logger.Log($\"Discord: Subscribed: {args.Event}\");\n        }\n\n        private void OnUnsubscribe(object sender, UnsubscribeMessage args)\n        {\n            Logger.Log($\"Discord: Unsubscribed: {args.Event}\");\n        }\n\n        #endregion\n\n        public void Dispose()\n        {\n            if (client == null)\n                return;\n\n            if (client.IsInitialized)\n                client.ClearPresence();\n\n            client.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/FinalSunSettings.cs",
    "content": "﻿using System.IO;\nusing System.Threading.Tasks;\nusing Rampastring.Tools;\nusing ClientCore;\nusing ClientCore.PlatformShim;\n\nnamespace DTAClient.Domain\n{\n    public static class FinalSunSettings\n    {\n        /// <summary>\n        /// Checks for the existence of the FinalSun settings file and writes it if it doesn't exist.\n        /// </summary>\n        public static void WriteFinalSunIniAsync()\n        {\n            Task.Run(DoWriteFinalSunIni);\n        }\n\n        private static void DoWriteFinalSunIni()\n        {\n            // The encoding of the FinalSun/FinalAlert ini file should be legacy ANSI, not Windows-1252 and also not any specific encoding.\n            // Otherwise, the map editor will not work in a non-ASCII path. ANSI doesn't mean a specific codepage,\n            // it means the default non-Unicode codepage which can be changed from Control Panel.\n            try\n            {\n                string finalSunIniPath = ClientConfiguration.Instance.FinalSunIniPath;\n                var finalSunIniFile = new FileInfo(Path.Combine(ProgramConstants.GamePath, finalSunIniPath));\n\n                Logger.Log(\"Checking for the existence of FinalSun.ini.\");\n                if (finalSunIniFile.Exists)\n                {\n                    Logger.Log(\"FinalSun settings file exists.\");\n\n                    IniFile iniFile = new IniFile();\n                    iniFile.FileName = finalSunIniFile.FullName;\n                    iniFile.Encoding = EncodingExt.ANSI;\n                    iniFile.Parse();\n\n                    iniFile.SetStringValue(\"FinalSun\", \"Language\", \"English\");\n                    iniFile.SetStringValue(\"FinalSun\", \"FileSearchLikeTS\", \"yes\");\n                    iniFile.SetStringValue(\"TS\", \"Exe\", SafePath.CombineDirectoryPath(ProgramConstants.GamePath));\n                    iniFile.WriteIniFile();\n\n                    return;\n                }\n\n                Logger.Log(\"FinalSun.ini doesn't exist - writing default settings.\");\n\n                if (!finalSunIniFile.Directory.Exists)\n                    finalSunIniFile.Directory.Create();\n\n                using var sw = new StreamWriter(finalSunIniFile.FullName, false, EncodingExt.ANSI);\n\n                sw.WriteLine(\"[FinalSun]\");\n                sw.WriteLine(\"Language=English\");\n                sw.WriteLine(\"FileSearchLikeTS=yes\");\n                sw.WriteLine(\"\");\n                sw.WriteLine(\"[TS]\");\n                sw.WriteLine(\"Exe=\" + SafePath.CombineDirectoryPath(ProgramConstants.GamePath));\n                sw.WriteLine(\"\");\n                sw.WriteLine(\"[UserInterface]\");\n                sw.WriteLine(\"EasyView=0\");\n                sw.WriteLine(\"NoSounds=0\");\n                sw.WriteLine(\"DisableAutoLat=0\");\n                sw.WriteLine(\"ShowBuildingCells=0\");\n            }\n            catch\n            {\n                Logger.Log(\"An exception occurred while checking the existence of FinalSun settings\");\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/MainClientConstants.cs",
    "content": "﻿using System;\nusing System.IO;\n\n#if WINFORMS\nusing System.Windows.Forms;\n#endif\nusing ClientCore;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain\n{\n    public static class MainClientConstants\n    {\n        public static string GAME_NAME_LONG = \"CnCNet Client\";\n        public static string GAME_NAME_SHORT = \"CnCNet\";\n        public static string SUPPORT_URL_SHORT = \"www.cncnet.org\";\n        public static bool USE_ISOMETRIC_CELLS = true;\n        public static int TDRA_WAYPOINT_COEFFICIENT = 128;\n        public static int MAP_CELL_SIZE_X = 48;\n        public static int MAP_CELL_SIZE_Y = 24;\n\n        public static OSVersion OSId = OSVersion.UNKNOWN;\n\n        // TODO: remove this variable after `Logger.Initialized` property is implemented by upstream\n        public static bool LoggerInitialized { get; set; } = false;\n\n        private static Action<string, string, bool> displayErrorAction = null;\n        /// <summary>\n        /// Gets or sets the action to perform to notify the user of an error.\n        /// </summary>\n        public static Action<string, string, bool> DisplayErrorAction\n        {\n            get => displayErrorAction ??= DefaultDisplayErrorAction;\n            set => displayErrorAction = value;\n        }\n\n        /// <summary>\n        /// Show an error in console as well as a Win32 MessageBox. For non-Windows platforms, this launches a text file in a GUI editor.\n        /// This action handles errors when XNA windows are not initialized yet.\n        /// </summary>\n        /// <param name=\"title\">The title.</param>\n        /// <param name=\"error\">The error.</param>\n        /// <param name=\"exit\">Whether the client exits.</param>\n        public static void DefaultDisplayErrorAction(string title, string error, bool exit)\n        {\n            Console.WriteLine(title);\n            Console.WriteLine();\n            Console.WriteLine(error);\n\n            if (LoggerInitialized)\n                Logger.Log(FormattableString.Invariant($\"{(title is null ? null : title + Environment.NewLine + Environment.NewLine)}{error}\"));\n\n#if WINFORMS\n            MessageBox.Show(error, title, MessageBoxButtons.OK, MessageBoxIcon.Error);\n#else\n            if (LoggerInitialized)\n                ProcessLauncher.StartShellProcess(ProgramConstants.LogFileName);\n            else\n            {\n                string tempfile = SafePath.CombineFilePath(Path.GetTempPath(), \"xna-cncnet-client-error.log\");\n                using (StreamWriter writer = new StreamWriter(tempfile))\n                {\n                    writer.WriteLine(title);\n                    writer.WriteLine();\n                    writer.WriteLine(error);\n                }\n                ProcessLauncher.StartShellProcess(tempfile);\n            }\n#endif\n\n            if (exit)\n                Environment.Exit(1);\n        }\n\n        public static void Initialize()\n        {\n            var clientConfiguration = ClientConfiguration.Instance;\n\n            OSId = clientConfiguration.GetOperatingSystemVersion();\n\n            GAME_NAME_SHORT = clientConfiguration.LocalGame;\n            GAME_NAME_LONG = clientConfiguration.LongGameName;\n            SUPPORT_URL_SHORT = clientConfiguration.ShortSupportURL;\n            USE_ISOMETRIC_CELLS = clientConfiguration.UseIsometricCells;\n            TDRA_WAYPOINT_COEFFICIENT = clientConfiguration.WaypointCoefficient;\n            MAP_CELL_SIZE_X = clientConfiguration.MapCellSizeX;\n            MAP_CELL_SIZE_Y = clientConfiguration.MapCellSizeY;\n\n            if (string.IsNullOrEmpty(GAME_NAME_SHORT))\n                throw new ClientConfigurationException(\"LocalGame is set to an empty value.\");\n\n            if (GAME_NAME_SHORT.Length > ProgramConstants.GAME_ID_MAX_LENGTH)\n            {\n                throw new ClientConfigurationException(\"LocalGame is set to a value that exceeds length limit of \" +\n                    ProgramConstants.GAME_ID_MAX_LENGTH + \" characters.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Mission.cs",
    "content": "﻿using System;\nusing System.Buffers.Binary;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text;\n\nusing ClientCore;\nusing ClientCore.Enums;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain\n{\n    /// <summary>\n    /// A Tiberian Sun mission listed in Battle(E).ini.\n    /// </summary>\n    public class Mission\n    {\n        public Mission(IniSection missionSection, string missionCodeName)\n        {\n            if (missionSection == null)\n                throw new ArgumentNullException(nameof(missionSection));\n\n            CD = missionSection.GetIntValue(nameof(CD), 0);\n            Side = missionSection.GetIntValue(nameof(Side), 0);\n            Scenario = missionSection.GetStringValue(nameof(Scenario), string.Empty);\n            UntranslatedGUIName = missionSection.GetStringValue(\"Description\", \"Undefined mission\");\n            GUIName = UntranslatedGUIName\n                .L10N($\"INI:Missions:{missionCodeName}:Description\");\n\n            IconPath = missionSection.GetStringValue(\"SideName\", string.Empty);\n            GUIDescription = missionSection.GetStringValue(\"LongDescription\", string.Empty)\n                .FromIniString()\n                .L10N($\"INI:Missions:{missionCodeName}:LongDescription\");\n            FinalMovie = missionSection.GetStringValue(nameof(FinalMovie), \"none\");\n            RequiredAddon = missionSection.GetBooleanValue(nameof(RequiredAddon),\n               ClientConfiguration.Instance.ClientGameType == ClientType.YR ||\n               ClientConfiguration.Instance.ClientGameType == ClientType.Ares ?\n                true :  // In case of YR this toggles Ra2Mode instead which should not be default\n                false\n            );\n            Enabled = missionSection.GetBooleanValue(nameof(Enabled), true);\n            BuildOffAlly = missionSection.GetBooleanValue(nameof(BuildOffAlly), false);\n            PlayerAlwaysOnNormalDifficulty = missionSection.GetBooleanValue(nameof(PlayerAlwaysOnNormalDifficulty), false);\n            Tags = missionSection.GetStringValue(nameof(Tags), string.Empty).Split(',');\n\n            CodeName = missionCodeName;\n            CustomMissionID = ComputeCustomMissionID(missionCodeName);\n            PreviewImage = missionSection.GetStringValue(\"PreviewImage\", string.Empty);\n        }\n\n        public static Mission NewCustomMission(IniSection clientMissionConfigSection, string missionCodeName, string scenario, IniSection? gameMissionConfigSection)\n        {\n            var mission = new Mission(clientMissionConfigSection, missionCodeName)\n            {\n                IsCustomMission = true,\n                Scenario = scenario,\n                GameMissionConfigSection = gameMissionConfigSection,\n                Tags = [\"CUSTOM\"],\n            };\n            return mission;\n        }\n\n        private static int ComputeCustomMissionID(string missionCodeName)\n        {\n#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms\n#pragma warning disable CA1850 // Prefer static 'HashData' method over 'ComputeHash'\n            using var sha1 = SHA1.Create();\n            byte[] digest = sha1.ComputeHash(Encoding.UTF8.GetBytes(missionCodeName));\n            return BinaryPrimitives.ReadInt32LittleEndian(digest);\n#pragma warning restore CA1850 // Prefer static 'HashData' method over 'ComputeHash'\n#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms\n        }\n\n        public string CodeName { get; private set; }\n        public int CampaignID { get; } = -1;\n        public int CustomMissionID { get; private set; }\n\n        public int CD { get; private set; }\n        public int Side { get; private set; }\n\n        /// <summary>\n        /// Refers to the map file. Must be a relative path to the game folder.\n        /// </summary>\n        public string Scenario { get; private set; }\n        public string GUIName { get; private set; }\n        public string UntranslatedGUIName { get; private set; }\n        public string IconPath { get; private set; }\n        public string GUIDescription { get; private set; }\n        public string FinalMovie { get; private set; }\n        public bool RequiredAddon { get; private set; }\n        public bool Enabled { get; set; }\n        public bool BuildOffAlly { get; private set; }\n        public bool PlayerAlwaysOnNormalDifficulty { get; private set; }\n        public IReadOnlyCollection<string> Tags { get; private set; }\n\n        /// <summary>\n        /// This property is not set through the ini file.\n        /// For a user custom mission, \"scenario\" will be assumed as the filename of a map file, with the suffix \".map\" (case-insensitive).\n        /// The map file is assumed to be placed at ClientConfiguration.CustomMissionPath.\n        /// 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.\n        /// </summary>\n        public bool IsCustomMission { get; private set; }\n\n        public IniSection? GameMissionConfigSection { get; set; }\n\n        public string PreviewImage { get; private set; }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/AllianceHolder.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Enums;\n\nusing Rampastring.Tools;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A helper class for setting up alliances in spawn.ini.\n    /// </summary>\n    public static class AllianceHolder\n    {\n        public static void WriteInfoToSpawnIni(\n            List<PlayerInfo> players,\n            List<PlayerInfo> aiPlayers, \n            List<int> multiCmbIndexes,\n            List<PlayerHouseInfo> playerHouseInfos,\n            List<TeamStartMapping> teamStartMappings,\n            IniFile spawnIni\n        )\n        {\n            List<int> team1MultiMemberIds = new List<int>();\n            List<int> team2MultiMemberIds = new List<int>();\n            List<int> team3MultiMemberIds = new List<int>();\n            List<int> team4MultiMemberIds = new List<int>();\n\n            for (int pId = 0; pId < players.Count; pId++)\n            {\n                var phi = playerHouseInfos[pId];\n                int teamId = players[pId].TeamId;\n                if (teamId <= 0)\n                    teamId = teamStartMappings?.Find(sa => sa.StartingWaypoint == phi.StartingWaypoint)?.TeamId ?? 0;\n\n                if (teamId > 0)\n                {\n                    switch (teamId)\n                    {\n                        case 1:\n                            team1MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1);\n                            break;\n                        case 2:\n                            team2MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1);\n                            break;\n                        case 3:\n                            team3MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1);\n                            break;\n                        case 4:\n                            team4MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1);\n                            break;\n                    }\n                }\n            }\n\n            int multiId = multiCmbIndexes.Count + 1;\n\n            for (int aiId = 0; aiId < aiPlayers.Count; aiId++)\n            {\n                var phi = playerHouseInfos[multiCmbIndexes.Count + aiId];\n                int teamId = aiPlayers[aiId].TeamId;\n                if (teamId <= 0)\n                    teamId = teamStartMappings?.Find(sa => sa.StartingWaypoint == phi.StartingWaypoint)?.TeamId ?? 0;\n\n\n                if (teamId > 0)\n                {\n                    switch (teamId)\n                    {\n                        case 1:\n                            team1MultiMemberIds.Add(multiId);\n                            break;\n                        case 2:\n                            team2MultiMemberIds.Add(multiId);\n                            break;\n                        case 3:\n                            team3MultiMemberIds.Add(multiId);\n                            break;\n                        case 4:\n                            team4MultiMemberIds.Add(multiId);\n                            break;\n                    }\n                }\n\n                multiId++;\n            }\n\n            WriteAlliances(team1MultiMemberIds, spawnIni);\n            WriteAlliances(team2MultiMemberIds, spawnIni);\n            WriteAlliances(team3MultiMemberIds, spawnIni);\n            WriteAlliances(team4MultiMemberIds, spawnIni);\n        }\n\n        private static void WriteAlliances(List<int> teamHouseMemberIds, IniFile spawnIni)\n        {\n            foreach (int houseId in teamHouseMemberIds)\n            {\n                bool selfFound = false;\n\n                for (int allyId = 0; allyId < teamHouseMemberIds.Count; allyId++)\n                {\n                    int allyHouseId = teamHouseMemberIds[allyId];\n\n                    if (allyHouseId == houseId)\n                        selfFound = true;\n                    else\n                    {\n                        spawnIni.SetIntValue(\"Multi\" + houseId + \"_Alliances\",\n                            \"HouseAlly\" + GetHouseAllyIndexString(allyId, selfFound),\n                            ClientConfiguration.Instance.ClientGameType == ClientType.RA\n                                ? allyHouseId + 11  // Compared with other games, Red Alert uses house IDs shifted by +12 (from -1 to +11) in multiplayer\n                                : allyHouseId - 1);\n                    }\n                }\n            }\n        }\n\n        private static string GetHouseAllyIndexString(int allyId, bool selfFound)\n        {\n            if (selfFound)\n                allyId = allyId - 1;\n\n            switch (allyId)\n            {\n                case 0:\n                    return \"One\";\n                case 1:\n                    return \"Two\";\n                case 2:\n                    return \"Three\";\n                case 3:\n                    return \"Four\";\n                case 4:\n                    return \"Five\";\n                case 5:\n                    return \"Six\";\n                case 6:\n                    return \"Seven\";\n            }\n\n            return \"None\" + allyId;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CacheManagerBase.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain.Multiplayer;\n\n/// <summary>\n/// Thread-safe manager for caching outputs with LRU eviction policy.\n/// Processes computation requests sequentially to limit CPU usage to a single thread.\n/// Note: this manager assumes the `TOutput` objects are managed, so it never disposes them directly.\n/// </summary>\npublic abstract class CacheManagerBase<TInput, TOutput> : ICacheManager<TInput, TOutput> where TInput : notnull\n{\n    public abstract string Name { get; }\n\n    private const int WorkerThreadShutdownTimeoutMs = 2000;\n\n    private readonly int capacity;\n    private readonly object cacheLock = new();\n    private readonly Dictionary<TInput, CacheEntry> cache = new();\n    private readonly LinkedList<TInput> lruList = new();\n    private readonly HashSet<TInput> requestQueue = new();\n    private readonly object queueLock = new();\n    private readonly Thread? workerThread;\n    private volatile bool isDisposed = false;\n\n    public int Count => cache.Count;\n\n    /// <summary>\n    /// Represents a cached TOutput entry with its position in the LRU list.\n    /// </summary>\n    private class CacheEntry\n    {\n        public TOutput? Output { get; }\n        public LinkedListNode<TInput> LruNode { get; set; }\n\n        public CacheEntry(TOutput? output, LinkedListNode<TInput> lruNode)\n        {\n            Output = output;\n            LruNode = lruNode;\n        }\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the InputPreviewCacheManager and start the worker thread immediately.\n    /// </summary>\n    /// <param name=\"capacity\">Maximum number of outputs to keep in cache. Must be positive.</param>\n    public CacheManagerBase(int capacity)\n    {\n        if (capacity <= 0)\n            throw new ArgumentException(\"Capacity must be positive.\", nameof(capacity));\n\n        this.capacity = capacity;\n\n        workerThread = new Thread(ProcessRequests)\n        {\n            IsBackground = true,\n            Name = $\"{Name}-Worker\"\n        };\n        workerThread.Start();\n    }\n\n    /// <summary>\n    /// Attempts to get a cached output for the specified input.\n    /// Updates LRU order if found.\n    /// </summary>\n    /// <param name=\"input\">The input.</param>\n    /// <param name=\"output\">The cached output if found.</param>\n    /// <returns>True if the output was found in cache; otherwise false.</returns>\n    private bool TryGet(TInput input, out TOutput? output)\n    {\n        if (input == null)\n            throw new ArgumentNullException(nameof(input));\n\n        lock (cacheLock)\n        {\n            if (cache.TryGetValue(input, out CacheEntry? entry))\n            {\n                // Move to front of LRU list (most recently used)\n                lruList.Remove(entry.LruNode);\n                entry.LruNode = lruList.AddFirst(input);\n                output = entry.Output;\n                return true;\n            }\n\n            output = default;\n            return false;\n        }\n    }\n\n    public bool Request(TInput input, out TOutput? output, bool syncLoadOnCacheMiss = false, bool addToQueue = true)\n    {\n        if (input == null)\n            throw new ArgumentNullException(nameof(input));\n\n        if (isDisposed)\n            throw new ObjectDisposedException(nameof(CacheManagerBase<TInput, TOutput>));\n\n        // Check if already cached\n        if (TryGet(input, out TOutput? cachedOutput))\n        {\n            output = cachedOutput;\n\n            return true;\n        }\n\n        // If not cached and sync load is allowed, attempt to load immediately (may be CPU-intensive)\n        if (syncLoadOnCacheMiss)\n        {\n            output = ComputeOutputForInput(input);\n\n            // Add to cache even if the output is null\n            AddToCache(input, output);\n\n            return true;\n        }\n\n        // Queue for processing (HashSet prevents duplicates)\n        if (addToQueue)\n        {\n            lock (queueLock)\n            {\n                if (requestQueue.Add(input))\n                {\n                    // Signal worker thread that new work is available\n                    Monitor.Pulse(queueLock);\n                }\n            }\n        }\n\n        output = default;\n\n        return false;\n    }\n\n    /// <summary>\n    /// Manually adds an output to the cache.\n    /// Useful for pre-loading or when output is obtained from other sources.\n    /// Note: If the input is already cached, this method updates LRU order but does NOT replace the cached output.\n    /// </summary>\n    /// <param name=\"input\">The input.</param>\n    /// <param name=\"output\">The output.</param>\n    /// <returns>True if the output was added to cache; false if output was already cached.</returns>\n    private bool AddToCache(TInput input, TOutput? output)\n    {\n        if (input == null)\n            throw new ArgumentNullException(nameof(input));\n\n        lock (cacheLock)\n        {\n            // If already cached, update LRU order but don't replace\n            if (cache.TryGetValue(input, out CacheEntry? existingEntry))\n            {\n                lruList.Remove(existingEntry.LruNode);\n                existingEntry.LruNode = lruList.AddFirst(input);\n                return false;\n            }\n\n            // Evict if at capacity\n            if (cache.Count >= capacity)\n                EvictLeastRecentlyUsed();\n\n            // Add new entry\n            LinkedListNode<TInput> node = lruList.AddFirst(input);\n            cache[input] = new CacheEntry(output, node);\n            return true;\n        }\n    }\n\n    public void Clear()\n    {\n        lock (cacheLock)\n        {\n            cache.Clear();\n            lruList.Clear();\n        }\n    }\n\n    /// <summary>\n    /// Computes the output for a given input. This method may or might not be called by the worker thread and may be CPU-intensive.\n    /// </summary>\n    /// <param name=\"input\">The input.</param>\n    /// <returns>The output.</returns>\n    protected abstract TOutput? ComputeOutputForInput(TInput input);\n\n    /// <summary>\n    /// Worker thread that processes computation requests sequentially.\n    /// </summary>\n    private void ProcessRequests()\n    {\n        while (!isDisposed)\n        {\n            TInput? input = default;\n            bool inputFound = false;\n\n            lock (queueLock)\n            {\n                // Wait for work or disposal\n                while (requestQueue.Count == 0 && !isDisposed)\n                {\n                    Monitor.Wait(queueLock);\n                }\n\n                // Exit if disposed\n                if (isDisposed)\n                    break;\n\n                // Get first item from HashSet\n                using var enumerator = requestQueue.GetEnumerator();\n                if (enumerator.MoveNext())\n                {\n                    inputFound = true;\n                    input = enumerator.Current;\n                    requestQueue.Remove(input);\n                }\n            }\n\n            // If no input, loop back to wait\n            if (!inputFound)\n                continue;\n\n            try\n            {\n                // Check if already cached (might have been computed by another request)\n                if (TryGet(input!, out _))\n                    continue;\n\n                // Get the output for the input. This is the CPU-intensive operation.\n                TOutput? output = ComputeOutputForInput(input!);\n\n                // Add to cache even if the output is null\n                AddToCache(input!, output);\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"{Name}: Failed to get the output for input '{input}'. Error: {ex.ToString()}\");\n            }\n        }\n    }\n\n    /// <summary>\n    /// Evicts the least recently used output from the cache.\n    /// Must be called within cacheLock.\n    /// </summary>\n    private void EvictLeastRecentlyUsed()\n    {\n        if (lruList.Last == null)\n            return;\n\n        TInput lruInput = lruList.Last.Value;\n        lruList.RemoveLast();\n\n        if (cache.TryGetValue(lruInput, out CacheEntry? entry))\n            cache.Remove(lruInput);\n    }\n\n    /// <summary>\n    /// Disposes the cache manager. Does not dispose cached outputs directly; left to garbage collector.\n    /// </summary>\n    public void Dispose()\n    {\n        if (isDisposed)\n            return;\n\n        isDisposed = true;\n\n        // Signal worker thread to stop\n        lock (queueLock)\n        {\n            Monitor.Pulse(queueLock);\n        }\n\n        // Wait for worker thread to finish\n        if (workerThread != null && workerThread.IsAlive)\n        {\n            if (!workerThread.Join(WorkerThreadShutdownTimeoutMs))\n            {\n                // Log warning if thread doesn't terminate gracefully\n                Logger.Log($\"{Name}: Worker thread did not terminate within timeout period.\");\n            }\n        }\n\n        // Clear cache\n        Clear();\n    }\n\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/CnCNetGame.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Threading;\n\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\n\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A class for games supported on CnCNet (DTA, TI, TS, RA1/2, etc.)\n    /// </summary>\n    public abstract class CnCNetGame\n    {\n        private readonly Lazy<Image?> lazyImage;\n        private readonly Lazy<Texture2D?> lazyTexture;\n\n        protected CnCNetGame()\n        {\n            lazyImage = new Lazy<Image?>(LoadImage, LazyThreadSafetyMode.ExecutionAndPublication);\n            lazyTexture = new Lazy<Texture2D?>(LoadTexture, LazyThreadSafetyMode.None);\n        }\n\n        /// <summary>\n        /// The name of the game that is displayed on the user-interface.\n        /// </summary>\n        public string? UIName { get; set; }\n\n        /// <summary>\n        /// The internal name (suffix) of the game.\n        /// </summary>\n        public string? InternalName { get; set; }\n\n        /// <summary>\n        /// The IRC chat channel ID of the game.\n        /// </summary>\n        public string? ChatChannel { get; set; }\n\n        /// <summary>\n        /// The IRC game broadcasting channel ID of the game.\n        /// </summary>\n        public string? GameBroadcastChannel { get; set; }\n\n        /// <summary>\n        /// The executable name of the game's client.\n        /// </summary>\n        public string? ClientExecutableName { get; set; }\n\n        /// <summary>\n        /// Gets the image for this game's icon. Loaded lazily and is thread-safe.\n        /// </summary>\n        public Image? Image => lazyImage.Value;\n\n        /// <summary>\n        /// Gets the texture for this game's icon. Loaded lazily; must be accessed only from the main (graphics) thread.\n        /// </summary>\n        public Texture2D? Texture => lazyTexture.Value;\n\n        /// <summary>\n        /// The location where to read the game's installation path from the registry.\n        /// </summary>\n        public string? RegistryInstallPath\n        {\n            get => field;\n            set\n            {\n                string? hive = value?.Split('\\\\')[0].Trim();\n                if (hive is not \"HKLM\" and not \"HKCU\")\n                    throw new Exception($\"Unexpected registry hive. Expected HKLM or HKCU. Got: {hive}\");\n\n                field = value;\n            }\n        }\n\n        private bool supported = true;\n\n        /// <summary>\n        /// Determines if the game is properly supported by this client.\n        /// Defaults to true.\n        /// </summary>\n        public bool Supported\n        {\n            get { return supported; }\n            set { supported = value; }\n        }\n\n        /// <summary>\n        /// If true, the client should always be connected to this game's chat channel.\n        /// </summary>\n        public bool AlwaysEnabled { get; set; }\n\n        /// <summary>\n        /// Loads the image for this game's icon. Thread-safe.\n        /// </summary>\n        protected abstract Image? LoadImage();\n\n        /// <summary>\n        /// Loads the texture for this game's icon. Must be called from the main (graphics) thread.\n        /// Note: the <see cref=\"Image\"/> instance is kept alive for the lifetime of this object,\n        /// since the lazy value cannot be explicitly disposed after texture creation. For the small\n        /// icon images used here this is an acceptable trade-off.\n        /// </summary>\n        protected virtual Texture2D? LoadTexture()\n        {\n            Image? image = Image;\n\n            if (image == null)\n                return null;\n\n            return AssetLoader.TextureFromImage(image);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs",
    "content": "#nullable enable\nusing ClientCore;\nusing System;\nusing System.Threading;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A class for updating of the CnCNet game/player count.\n    /// </summary>\n    public static class CnCNetPlayerCountTask\n    {\n        public static int PlayerCount { get; private set; }\n\n        private static int REFRESH_INTERVAL = 60000; // 1 minute\n\n        internal static event EventHandler<PlayerCountEventArgs>? CnCNetGameCountUpdated;\n\n        private static string? cncnetLiveStatusIdentifier;\n\n        public static void InitializeService(CancellationTokenSource cts)\n        {\n            cncnetLiveStatusIdentifier = ClientConfiguration.Instance.CnCNetLiveStatusIdentifier;\n\n            // This call is synchronous. Therefore, we use a short timeout to avoid blocking the main thread for too long.\n            PlayerCount = GetCnCNetPlayerCount(timeoutMilliseconds: 1000);\n\n            CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(PlayerCount));\n            ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts);\n        }\n\n        private static void RunService(object? tokenObj)\n        {\n            var waitHandle = ((CancellationTokenSource)tokenObj!).Token.WaitHandle;\n\n            while (true)\n            {\n                if (waitHandle.WaitOne(REFRESH_INTERVAL))\n                {\n                    // Cancellation signaled\n                    return;\n                }\n                else\n                {\n                    CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(GetCnCNetPlayerCount(timeoutMilliseconds: 5000)));\n                }\n            }\n        }\n\n        private static int GetCnCNetPlayerCount(int timeoutMilliseconds = 5000)\n        {\n            try\n            {\n                // Don't fetch the player count if it is explicitly disabled.\n                // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship,\n                // which causes lags in the splash screen. In the worst case, say if packets are dropped, it waits until timeouts.\n                if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetPlayerCountURL))\n                    return -1;\n\n                string info = new TimedHttpClient(timeoutMilliseconds)\n                    .GetString(ClientConfiguration.Instance.CnCNetPlayerCountURL);\n\n                info = info.Replace(\"{\", string.Empty);\n                info = info.Replace(\"}\", string.Empty);\n                info = info.Replace(\"\\\"\", string.Empty);\n                string[] values = info.Split(new char[] { ',' });\n\n                int numGames = -1;\n\n                foreach (string value in values)\n                {\n                    if (value.Contains(cncnetLiveStatusIdentifier!))\n                    {\n                        numGames = Convert.ToInt32(value.Substring(cncnetLiveStatusIdentifier!.Length + 1));\n                        return numGames;\n                    }\n                }\n\n                return numGames;\n            }\n            catch\n            {\n                return -1;\n            }\n        }\n    }\n\n    internal class PlayerCountEventArgs : EventArgs\n    {\n        public PlayerCountEventArgs(int playerCount)\n        {\n            PlayerCount = playerCount;\n        }\n\n        public int PlayerCount { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs",
    "content": "﻿using Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net;\nusing System.Net.NetworkInformation;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A CnCNet tunnel server.\n    /// </summary>\n    public class CnCNetTunnel\n    {\n        private const int REQUEST_TIMEOUT = 10000; // In milliseconds\n        private const int PING_TIMEOUT = 1000;\n\n        public CnCNetTunnel() { }\n\n        /// <summary>\n        /// Parses a formatted string that contains the tunnel server's \n        /// information into a CnCNetTunnel instance.\n        /// </summary>\n        /// <param name=\"str\">The string that contains the tunnel server's information.</param>\n        /// <returns>A CnCNetTunnel instance parsed from the given string.</returns>\n        public static CnCNetTunnel Parse(string str)\n        {\n            // For the format, check http://cncnet.org/master-list\n\n            try\n            {\n                var tunnel = new CnCNetTunnel();\n                string[] parts = str.Split(';');\n\n                string address = parts[0];\n                string[] detailedAddress = address.Split(new char[] { ':' });\n\n                tunnel.Address = detailedAddress[0];\n                tunnel.Port = int.Parse(detailedAddress[1]);\n                tunnel.Country = parts[1];\n                tunnel.CountryCode = parts[2];\n                tunnel.Name = parts[3];\n                tunnel.RequiresPassword = parts[4] != \"0\";\n                tunnel.Clients = int.Parse(parts[5]);\n                tunnel.MaxClients = int.Parse(parts[6]);\n                int status = int.Parse(parts[7]);\n                tunnel.Official = status == 2;\n                if (!tunnel.Official)\n                    tunnel.Recommended = status == 1;\n\n                CultureInfo cultureInfo = CultureInfo.InvariantCulture;\n\n                tunnel.Latitude = double.Parse(parts[8], cultureInfo);\n                tunnel.Longitude = double.Parse(parts[9], cultureInfo);\n                tunnel.Version = int.Parse(parts[10]);\n                tunnel.Distance = double.Parse(parts[11], cultureInfo);\n\n                return tunnel;\n            }\n            catch (Exception ex)\n            {\n                if (ex is FormatException || ex is OverflowException || ex is IndexOutOfRangeException)\n                {\n                    Logger.Log(\"Parsing tunnel information failed: \" + ex.ToString() + Environment.NewLine + \"Parsed string: \" + str);\n                    return null;\n                }\n\n                throw;\n            }\n        }\n\n        public string Address { get; private set; }\n        public int Port { get; private set; }\n        public string Country { get; private set; }\n        public string CountryCode { get; private set; }\n        public string Name { get; private set; }\n        public bool RequiresPassword { get; private set; }\n        public int Clients { get; private set; }\n        public int MaxClients { get; private set; }\n        public bool Official { get; private set; }\n        public bool Recommended { get; private set; }\n        public double Latitude { get; private set; }\n        public double Longitude { get; private set; }\n        public int Version { get; private set; }\n        public double Distance { get; private set; }\n        public int PingInMs { get; set; } = -1;\n\n        /// <summary>\n        /// Updates this tunnel's metadata from another tunnel instance, preserving Address, Port, and existing PingInMs.\n        /// </summary>\n        internal void UpdateFrom(CnCNetTunnel updatedTunnel)\n        {\n            Country = updatedTunnel.Country;\n            CountryCode = updatedTunnel.CountryCode;\n            Name = updatedTunnel.Name;\n            Clients = updatedTunnel.Clients;\n            MaxClients = updatedTunnel.MaxClients;\n            Official = updatedTunnel.Official;\n            Recommended = updatedTunnel.Recommended;\n            Version = updatedTunnel.Version;\n\n            RequiresPassword = updatedTunnel.RequiresPassword;\n            Latitude = updatedTunnel.Latitude;\n            Longitude = updatedTunnel.Longitude;\n            Distance = updatedTunnel.Distance;\n        }\n\n        /// <summary>\n        /// Gets a list of player ports to use from a specific V2 tunnel server.\n        /// </summary>\n        /// <returns>A list of player ports to use.</returns>\n        public List<int> GetPlayerPortInfo(int playerCount)\n        {\n            try\n            {\n                Logger.Log($\"Contacting tunnel at {Address}:{Port}\");\n\n                // Do not use https here as not supported by tunnels\n                string addressString = $\"http://{Address}:{Port}/request?clients={playerCount}\";\n                Logger.Log($\"Downloading from {addressString}\");\n\n                string data = new TimedHttpClient(REQUEST_TIMEOUT).GetString(addressString);\n\n                data = data.Replace(\"[\", String.Empty);\n                data = data.Replace(\"]\", String.Empty);\n\n                string[] portIDs = data.Split(',');\n                List<int> playerPorts = new List<int>();\n\n                foreach (string _port in portIDs)\n                {\n                    playerPorts.Add(Convert.ToInt32(_port));\n                    Logger.Log($\"Added port {_port}\");\n                }\n\n                return playerPorts;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Unable to connect to the specified tunnel server. Returned error message: \" + ex.ToString());\n            }\n\n            return new List<int>();\n        }\n\n        public void UpdatePing()\n        {\n            using (Ping p = new Ping())\n            {\n                try\n                {\n                    PingReply reply = p.Send(IPAddress.Parse(Address), PING_TIMEOUT);\n                    if (reply.Status == IPStatus.Success)\n                        PingInMs = Convert.ToInt32(reply.RoundtripTime);\n                }\n                catch (PingException ex)\n                {\n                    Logger.Log($\"Caught an exception when pinging {Name} tunnel server: {ex.ToString()}\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/CustomCnCNetGame.cs",
    "content": "#nullable enable\nusing System;\nusing System.Reflection;\n\nusing Microsoft.Xna.Framework.Graphics;\n\nusing Rampastring.XNAUI;\n\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A <see cref=\"CnCNetGame\"/> that loads its texture from a custom icon file, or falls back\n    /// to the unknown game icon embedded in the assembly.\n    /// </summary>\n    internal sealed class CustomCnCNetGame : CnCNetGame\n    {\n        private static readonly Lazy<Image> lazyFallbackImage = new(() =>\n            Image.Load(\n                Assembly.GetAssembly(typeof(CustomCnCNetGame))!\n                .GetManifestResourceStream(\"DTAClient.Icons.unknownicon.png\")));\n        private static Image FallbackImage => lazyFallbackImage.Value;\n\n        private readonly string iconFilename;\n\n        public CustomCnCNetGame(string iconFilename)\n        {\n            this.iconFilename = iconFilename;\n        }\n\n        protected override Image? LoadImage() => FallbackImage;\n\n        protected override Texture2D? LoadTexture()\n        {\n            if (AssetLoader.AssetExists(iconFilename))\n                return AssetLoader.LoadTexture(iconFilename);\n\n            return base.LoadTexture();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/DefaultCnCNetGame.cs",
    "content": "#nullable enable\nusing System.Reflection;\n\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A <see cref=\"CnCNetGame\"/> that loads its icon from an embedded assembly resource.\n    /// </summary>\n    internal sealed class DefaultCnCNetGame : CnCNetGame\n    {\n        private static readonly Assembly assembly = Assembly.GetAssembly(typeof(DefaultCnCNetGame))!;\n\n        private readonly string iconResourceName;\n\n        public DefaultCnCNetGame(string iconResourceName)\n        {\n            this.iconResourceName = iconResourceName;\n        }\n\n        protected override Image? LoadImage()\n        {\n            using var stream = assembly.GetManifestResourceStream(iconResourceName);\n\n            if (stream == null)\n                return null;\n\n            return Image.Load(stream);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/GameCollection.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\nusing System;\nusing System.Threading.Tasks;\nusing Rampastring.Tools;\nusing ClientCore;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// A class for storing the collection of supported CnCNet games.\n    /// </summary>\n    public class GameCollection\n    {\n        public List<CnCNetGame> GameList { get; private set; }\n\n        public GameCollection()\n        {\n            Initialize();\n        }\n\n        public void Initialize()\n        {\n            GameList = new List<CnCNetGame>();\n\n            // Default supported games. Images are loaded lazily in background threads;\n            // textures are created on demand on the main thread when first accessed.\n            var defaultGames = new DefaultCnCNetGame[]\n            {\n                new DefaultCnCNetGame(\"DTAClient.Icons.dtaicon.png\")\n                {\n                    ChatChannel = \"#cncnet-dta\",\n                    ClientExecutableName = \"DTA.exe\",\n                    GameBroadcastChannel = \"#cncnet-dta-games\",\n                    InternalName = \"dta\",\n                    RegistryInstallPath = \"HKCU\\\\Software\\\\TheDawnOfTheTiberiumAge\",\n                    UIName = \"Dawn of the Tiberium Age\".L10N(\"Client:ClientCore:DawnoftheTiberiumAge\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.tiicon.png\")\n                {\n                    ChatChannel = \"#cncnet-ti\",\n                    ClientExecutableName = \"TI_Launcher.exe\",\n                    GameBroadcastChannel = \"#cncnet-ti-games\",\n                    InternalName = \"ti\",\n                    RegistryInstallPath = \"HKCU\\\\Software\\\\TwistedInsurrection\",\n                    UIName = \"Twisted Insurrection\".L10N(\"Client:ClientCore:TwistedInsurrection\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.moicon.png\")\n                {\n                    ChatChannel = \"#cncnet-mo\",\n                    ClientExecutableName = \"MentalOmegaClient.exe\",\n                    GameBroadcastChannel = \"#cncnet-mo-games\",\n                    InternalName = \"mo\",\n                    RegistryInstallPath = \"HKCU\\\\Software\\\\MentalOmega\",\n                    UIName = \"Mental Omega\".L10N(\"Client:ClientCore:MentalOmega\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.rricon.png\")\n                {\n                    ChatChannel = \"#redres-lobby\",\n                    ClientExecutableName = \"RRLauncher.exe\",\n                    GameBroadcastChannel = \"#redres-games\",\n                    InternalName = \"rr\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\RedResurrection\",\n                    UIName = \"YR Red-Resurrection\".L10N(\"Client:ClientCore:YRRedResurrection\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.reicon.png\")\n                {\n                    ChatChannel = \"#riseoftheeast\",\n                    ClientExecutableName = \"RELauncher.exe\",\n                    GameBroadcastChannel = \"#rote-games\",\n                    InternalName = \"re\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\RiseoftheEast\",\n                    UIName = \"Rise of the East\".L10N(\"Client:ClientCore:RiseoftheEast\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.cncricon.png\")\n                {\n                    ChatChannel = \"#cncreloaded\",\n                    ClientExecutableName = \"CnCReloadedClient.exe\",\n                    GameBroadcastChannel = \"#cncreloaded-games\",\n                    InternalName = \"cncr\",\n                    RegistryInstallPath = \"HKCU\\\\Software\\\\CnCReloaded\",\n                    UIName = \"C&C: Reloaded\".L10N(\"Client:ClientCore:CnCReloaded\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.tdicon.png\")\n                {\n                    ChatChannel = \"#cncnet-td\",\n                    ClientExecutableName = \"TiberianDawn.exe\",\n                    GameBroadcastChannel = \"#cncnet-td-games\",\n                    InternalName = \"td\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Tiberian Dawn\",\n                    UIName = \"Tiberian Dawn\".L10N(\"Client:ClientCore:TiberianDawn\"),\n                    Supported = false\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.raicon.png\")\n                {\n                    ChatChannel = \"#cncnet-ra\",\n                    ClientExecutableName = \"RedAlert.exe\",\n                    GameBroadcastChannel = \"#cncnet-ra-games\",\n                    InternalName = \"ra\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Red Alert\",\n                    UIName = \"Red Alert\".L10N(\"Client:ClientCore:RedAlert\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.d2kicon.png\")\n                {\n                    ChatChannel = \"#cncnet-d2k\",\n                    ClientExecutableName = \"Dune2000.exe\",\n                    GameBroadcastChannel = \"#cncnet-d2k-games\",\n                    InternalName = \"d2k\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Dune 2000\",\n                    UIName = \"Dune 2000\".L10N(\"Client:ClientCore:Dune2000\"),\n                    Supported = false\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.tsicon.png\")\n                {\n                    ChatChannel = \"#cncnet-ts\",\n                    ClientExecutableName = \"TiberianSun.exe\",\n                    GameBroadcastChannel = \"#cncnet-ts-games\",\n                    InternalName = \"ts\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Tiberian Sun\",\n                    UIName = \"Tiberian Sun\".L10N(\"Client:ClientCore:TiberianSun\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.yricon.png\")\n                {\n                    ChatChannel = \"#cncnet-yr\",\n                    ClientExecutableName = \"CnCNetClientYR.exe\",\n                    GameBroadcastChannel = \"#cncnet-yr-games\",\n                    InternalName = \"yr\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Yuri's Revenge\",\n                    UIName = \"Yuri's Revenge\".L10N(\"Client:ClientCore:YurisRevenge\")\n                },\n\n                new DefaultCnCNetGame(\"DTAClient.Icons.ssicon.png\")\n                {\n                    ChatChannel = \"#cncnet-ss\",\n                    ClientExecutableName = \"SoleSurvivor.exe\",\n                    GameBroadcastChannel = \"#cncnet-ss-games\",\n                    InternalName = \"ss\",\n                    RegistryInstallPath = \"HKLM\\\\Software\\\\Westwood\\\\Sole Survivor\",\n                    UIName = \"Sole Survivor\".L10N(\"Client:ClientCore:SoleSurvivor\"),\n                    Supported = false\n                }\n            };\n\n            // CnCNet chat.\n            var otherGames = new DefaultCnCNetGame[]\n            {\n                new DefaultCnCNetGame(\"DTAClient.Icons.cncneticon.png\")\n                {\n                    ChatChannel = \"#cncnet\",\n                    InternalName = \"cncnet\",\n                    UIName = \"General CnCNet Chat\".L10N(\"Client:ClientCore:GeneralCnCNetChat\"),\n                    AlwaysEnabled = true\n                }\n            };\n\n            GameList.AddRange(defaultGames);\n            GameList.AddRange(GetCustomGames(defaultGames.Concat<CnCNetGame>(otherGames).ToList()));\n            GameList.AddRange(otherGames);\n\n            if (GetGameIndexFromInternalName(ClientConfiguration.Instance.LocalGame) == -1)\n            {\n                throw new ClientConfigurationException(\"Could not find a game in the game collection matching LocalGame value of \" +\n                    ClientConfiguration.Instance.LocalGame + \".\");\n            }\n\n            // Fire-and-forget background preloading of images.\n            var gamesToPreload = GameList.ToList();\n            _ = Task.Run(() =>\n            {\n                foreach (var game in gamesToPreload)\n                    _ = game.Image;\n            });\n        }\n\n        private List<CnCNetGame> GetCustomGames(List<CnCNetGame> existingGames)\n        {\n            IniFile iniFile = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"GameCollectionConfig.ini\"));\n\n            List<CnCNetGame> customGames = new List<CnCNetGame>();\n\n            var section = iniFile.GetSection(\"CustomGames\");\n\n            if (section == null)\n                return customGames;\n\n            HashSet<string> customGameIDs = new HashSet<string>();\n            foreach (var kvp in section.Keys)\n            {\n                if (!iniFile.SectionExists(kvp.Value))\n                    continue;\n\n                string ID = iniFile.GetStringValue(kvp.Value, \"InternalName\", string.Empty).ToLowerInvariant();\n\n                if (string.IsNullOrEmpty(ID))\n                    throw new GameCollectionConfigurationException(\"InternalName for game \" + kvp.Value + \" is not defined or set to an empty value.\");\n\n                if (ID.Length > ProgramConstants.GAME_ID_MAX_LENGTH)\n                {\n                    throw new GameCollectionConfigurationException(\"InternalGame for game \" + kvp.Value + \" is set to a value that exceeds length limit of \" +\n                        ProgramConstants.GAME_ID_MAX_LENGTH + \" characters.\");\n                }\n\n                if (existingGames.Find(g => g.InternalName == ID) != null || customGameIDs.Contains(ID))\n                    throw new GameCollectionConfigurationException(\"Game with InternalName \" + ID.ToUpperInvariant() + \" already exists in the game collection.\");\n\n                string iconFilename = iniFile.GetStringValue(kvp.Value, \"IconFilename\", ID + \"icon.png\");\n\n                CustomCnCNetGame customCnCNetGame;\n                try\n                {\n                    customCnCNetGame = new CustomCnCNetGame(iconFilename)\n                    {\n                        InternalName = ID,\n                        UIName = iniFile.GetStringValue(kvp.Value, \"UIName\", ID.ToUpperInvariant()),\n                        ChatChannel = GetIRCChannelNameFromIniFile(iniFile, kvp.Value, \"ChatChannel\"),\n                        GameBroadcastChannel = GetIRCChannelNameFromIniFile(iniFile, kvp.Value, \"GameBroadcastChannel\"),\n                        ClientExecutableName = iniFile.GetStringValue(kvp.Value, \"ClientExecutableName\", string.Empty),\n                        RegistryInstallPath = iniFile.GetStringValue(kvp.Value, \"RegistryInstallPath\", \"HKCU\\\\Software\\\\\"\n                            + ID.ToUpperInvariant())\n                    };\n                }\n                catch (Exception ex)\n                {\n                    throw new GameCollectionConfigurationException(\"Error while reading GameCollectionConfig.ini for game \" + kvp.Value + \": \" + ex.Message);\n                }\n\n                customGames.Add(customCnCNetGame);\n                customGameIDs.Add(ID);\n            }\n\n            return customGames;\n        }\n\n        private string GetIRCChannelNameFromIniFile(IniFile iniFile, string section, string key)\n        {\n            string channel = iniFile.GetStringValue(section, key, string.Empty);\n\n            if (string.IsNullOrEmpty(channel))\n                throw new GameCollectionConfigurationException(key + \" for game \" + section + \" is not defined or set to an empty value.\");\n\n            if (channel.Contains(' ') || channel.Contains(',') || channel.Contains((char)7))\n                throw new GameCollectionConfigurationException(key + \" for game \" + section + \" contains characters not allowed on IRC channel names.\");\n\n            if (!channel.StartsWith(\"#\"))\n                return \"#\" + channel;\n\n            return channel;\n        }\n\n        /// <summary>\n        /// Gets the index of a CnCNet supported game based on its internal name.\n        /// </summary>\n        /// <param name=\"gameName\">The internal name (suffix) of the game.</param>\n        /// <returns>The index of the specified CnCNet game. -1 if the game is unknown or not supported.</returns>\n        public int GetGameIndexFromInternalName(string gameName)\n        {\n            for (int gId = 0; gId < GameList.Count; gId++)\n            {\n                CnCNetGame game = GameList[gId];\n\n                if (gameName.ToLowerInvariant() == game.InternalName)\n                    return gId;\n            }\n\n            return -1;\n        }\n\n        /// <summary>\n        /// Seeks the supported game list for a specific game's internal name and if found,\n        /// returns the game's full name. Otherwise returns the internal name specified in the param.\n        /// </summary>\n        /// <param name=\"gameName\">The internal name of the game to seek for.</param>\n        /// <returns>The full name of a supported game based on its internal name.\n        /// Returns the given parameter if the name isn't found in the supported game list.</returns>\n        public string GetGameNameFromInternalName(string gameName)\n        {\n            CnCNetGame game = GameList.Find(g => g.InternalName == gameName.ToLowerInvariant());\n\n            if (game == null)\n                return gameName;\n\n            return game.UIName;\n        }\n\n        /// <summary>\n        /// Returns the full UI name of a game based on its index in the game list.\n        /// </summary>\n        /// <param name=\"gameIndex\">The index of the CnCNet supported game.</param>\n        /// <returns>The UI name of the game.</returns>\n        public string GetFullGameNameFromIndex(int gameIndex)\n        {\n            return GameList[gameIndex].UIName;\n        }\n\n        /// <summary>\n        /// Returns the internal name of a game based on its index in the game list.\n        /// </summary>\n        /// <param name=\"gameIndex\">The index of the CnCNet supported game.</param>\n        /// <returns>The internal name (suffix) of the game.</returns>\n        public string GetGameIdentifierFromIndex(int gameIndex)\n        {\n            return GameList[gameIndex].InternalName;\n        }\n\n        public string GetGameBroadcastingChannelNameFromIdentifier(string gameIdentifier)\n        {\n            CnCNetGame game = GameList.Find(g => g.InternalName == gameIdentifier.ToLowerInvariant());\n            if (game == null)\n                return null;\n            return game.GameBroadcastChannel;\n        }\n\n        public string GetGameChatChannelNameFromIdentifier(string gameIdentifier)\n        {\n            CnCNetGame game = GameList.Find(g => g.InternalName == gameIdentifier.ToLowerInvariant());\n            if (game == null)\n                return null;\n            return game.ChatChannel;\n        }\n    }\n\n    /// <summary>\n    /// An exception that is thrown when configuration for a game to add to game collection\n    /// contains invalid or unexpected settings / data or required settings / data are missing.\n    /// </summary>\n    class GameCollectionConfigurationException : Exception\n    {\n        public GameCollectionConfigurationException(string message) : base(message)\n        {\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    public class HostedCnCNetGame : GenericHostedGame\n    {\n        public HostedCnCNetGame() { }\n\n        public HostedCnCNetGame(string channelName, string revision, string gamever, int maxPlayers,\n            string roomName, bool passworded,\n            bool tunneled,\n            string[] players, string adminName, string mapName, string gameMode, string mapHash)\n        {\n            ChannelName = channelName;\n            Revision = revision;\n            GameVersion = gamever;\n            MaxPlayers = maxPlayers;\n            RoomName = roomName;\n            Passworded = passworded;\n            Tunneled = tunneled;\n            Players = players;\n            HostName = adminName;\n            Map = mapName;\n            GameMode = gameMode;\n            MapHash = mapHash;\n        }\n\n        public string ChannelName { get; set; }\n        public string Revision { get; set; }\n        public bool Tunneled { get; set; }\n        public bool IsLadder { get; set; }\n        public string MatchID { get; set; }\n        public CnCNetTunnel TunnelServer { get; set; }\n        public int[] BroadcastedGameOptionValues { get; set; }\n\n        public override int Ping => TunnelServer.PingInMs;\n\n        public override bool Equals(GenericHostedGame other)\n            => other is HostedCnCNetGame hostedCnCNetGame\n                ? string.Equals(hostedCnCNetGame.ChannelName, ChannelName, StringComparison.InvariantCultureIgnoreCase)\n                : base.Equals(other);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/MapEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    public class MapEventArgs : EventArgs\n    {\n        public MapEventArgs(Map map)\n        {\n            Map = map;\n        }\n\n        public Map Map { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.IO;\nusing System.Globalization;\nusing System.Net.Http;\nusing System.Threading;\nusing Rampastring.Tools;\nusing ClientCore;\nusing System.IO.Compression;\nusing System.Linq;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// Handles sharing maps.\n    /// </summary>\n    public static class MapSharer\n    {\n        public static event EventHandler<MapEventArgs> MapUploadFailed;\n\n        public static event EventHandler<MapEventArgs> MapUploadComplete;\n\n        public static event EventHandler<MapEventArgs> MapUploadStarted;\n\n        public static event EventHandler<SHA1EventArgs> MapDownloadFailed;\n\n        public static event EventHandler<SHA1EventArgs> MapDownloadComplete;\n\n        public static event EventHandler<SHA1EventArgs> MapDownloadStarted;\n\n        private volatile static List<string> MapDownloadQueue = new List<string>();\n        private volatile static List<Map> MapUploadQueue = new List<Map>();\n        private volatile static List<string> UploadedMaps = new List<string>();\n\n        private static readonly object locker = new object();\n\n        private const int DOWNLOAD_TIMEOUT = 100000; // In milliseconds\n        private const int UPLOAD_TIMEOUT = 100000; // In milliseconds\n\n        /// <summary>\n        /// Adds a map into the CnCNet map upload queue.\n        /// </summary>\n        /// <param name=\"map\">The map.</param>\n        /// <param name=\"myGame\">The short name of the game that is being played (DTA, TI, MO, etc).</param>\n        public static void UploadMap(Map map, string myGame)\n        {\n            lock (locker)\n            {\n                if (UploadedMaps.Contains(map.SHA1) || MapUploadQueue.Contains(map))\n                {\n                    Logger.Log(\"MapSharer: Already uploading map \" + map.BaseFilePath + \" - returning.\");\n                    return;\n                }\n\n                MapUploadQueue.Add(map);\n\n                if (MapUploadQueue.Count == 1)\n                {\n                    ParameterizedThreadStart pts = new ParameterizedThreadStart(Upload);\n                    Thread thread = new Thread(pts);\n                    object[] mapAndGame = new object[2];\n                    mapAndGame[0] = map;\n                    mapAndGame[1] = myGame.ToLower();\n                    thread.Start(mapAndGame);\n                }\n            }\n        }\n\n        private static void Upload(object mapAndGame)\n        {\n            object[] mapGameArray = (object[])mapAndGame;\n\n            Map map = (Map)mapGameArray[0];\n            string myGameId = (string)mapGameArray[1];\n\n            MapUploadStarted?.Invoke(null, new MapEventArgs(map));\n\n            Logger.Log(\"MapSharer: Starting upload of \" + map.BaseFilePath);\n\n            if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBUploadURL))\n            {\n                Logger.Log(\"MapSharer: Upload URL is not configured.\");\n                MapUploadFailed?.Invoke(null, new MapEventArgs(map));\n                return;\n            }\n\n            string message = MapUpload(ClientConfiguration.Instance.CnCNetMapDBUploadURL, map, myGameId, out bool success);\n\n            if (success)\n            {\n                MapUploadComplete?.Invoke(null, new MapEventArgs(map));\n\n                lock (locker)\n                {\n                    UploadedMaps.Add(map.SHA1);\n                }\n\n                Logger.Log(\"MapSharer: Uploading map \" + map.BaseFilePath + \" completed succesfully.\");\n            }\n            else\n            {\n                MapUploadFailed?.Invoke(null, new MapEventArgs(map));\n\n                Logger.Log(\"MapSharer: Uploading map \" + map.BaseFilePath + \" failed! Returned message: \" + message);\n            }\n\n            lock (locker)\n            {\n                MapUploadQueue.Remove(map);\n\n                if (MapUploadQueue.Count > 0)\n                {\n                    Map nextMap = MapUploadQueue[0];\n\n                    object[] array = new object[2];\n                    array[0] = nextMap;\n                    array[1] = myGameId;\n\n                    Logger.Log(\"MapSharer: There are additional maps in the queue.\");\n\n                    Upload(array);\n                }\n            }\n        }\n\n        private static string MapUpload(string _URL, Map map, string gameName, out bool success)\n        {\n            FileInfo zipFile = SafePath.GetFile(ProgramConstants.GamePath, \"Maps\", \"Custom\", FormattableString.Invariant($\"{map.SHA1}.zip\"));\n\n            if (zipFile.Exists) zipFile.Delete();\n\n            string mapFileName = $\"{map.SHA1}.{ClientConfiguration.Instance.MapFileExtension}\";\n\n            File.Copy(SafePath.CombineFilePath(map.CompleteFilePath), SafePath.CombineFilePath(ProgramConstants.GamePath, mapFileName));\n\n            CreateZipFile(mapFileName, zipFile.FullName);\n\n            try\n            {\n                SafePath.DeleteFileIfExists(ProgramConstants.GamePath, mapFileName);\n            }\n            catch { }\n\n            // Upload the file to the URI. \n            // The 'UploadFile(uriString,fileName)' method implicitly uses HTTP POST method. \n\n            try\n            {\n                using (FileStream stream = zipFile.Open(FileMode.Open))\n                {\n                    List<FileToUpload> files = new List<FileToUpload>();\n                    //{\n                    //    new FileToUpload\n                    //    {\n                    //        Name = \"file\",\n                    //        Filename = Path.GetFileName(zipFile),\n                    //        ContentType = \"mapZip\",\n                    //        Stream = stream\n                    //    };\n                    //};\n\n                    FileToUpload file = new FileToUpload()\n                    {\n                        Name = \"file\",\n                        Filename = zipFile.Name,\n                        ContentType = \"mapZip\",\n                        Stream = stream\n                    };\n\n                    files.Add(file);\n\n                    byte[] responseArray = UploadFiles(_URL, files, gameName.ToLower());\n                    string response = Encoding.UTF8.GetString(responseArray);\n\n                    if (!response.Contains(\"Upload succeeded!\"))\n                    {\n                        success = false;\n                        return response;\n                    }\n                    Logger.Log(\"MapSharer: Upload response: \" + response);\n\n                    //MessageBox.Show((response));\n\n                    success = true;\n                    return String.Empty;\n                }\n            }\n            catch (Exception ex)\n            {\n                success = false;\n                return ex.Message;\n            }\n        }\n\n        private static byte[] UploadFiles(string address, List<FileToUpload> files, string gameName)\n        {\n            using var content = new MultipartFormDataContent();\n\n            content.Add(new StringContent(gameName), \"game\");\n\n            foreach (FileToUpload file in files)\n            {\n                var streamContent = new StreamContent(file.Stream);\n                streamContent.Headers.TryAddWithoutValidation(\"Content-Type\", file.ContentType);\n                content.Add(streamContent, file.Name, file.Filename);\n            }\n\n            return new TimedHttpClient(UPLOAD_TIMEOUT).Post(address, content);\n        }\n\n        private static void CreateZipFile(string file, string zipName)\n        {\n            using var zipFileStream = new FileStream(zipName, FileMode.CreateNew, FileAccess.Write);\n            using var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create);\n            archive.CreateEntryFromFile(SafePath.CombineFilePath(ProgramConstants.GamePath, file), file);\n        }\n\n        private static string ExtractZipFile(string zipFile, string destDir)\n        {\n            using ZipArchive zipArchive = ZipFile.OpenRead(zipFile);\n\n            // here, we extract every entry, but we could extract conditionally\n            // based on entry name, size, date, checkbox status, etc.  \n            zipArchive.ExtractToDirectory(destDir);\n\n            return zipArchive.Entries.FirstOrDefault()?.Name;\n        }\n\n        public static void DownloadMap(string sha1, string myGame, string mapName)\n        {\n            lock (locker)\n            {\n                if (MapDownloadQueue.Contains(sha1))\n                {\n                    Logger.Log(\"MapSharer: Map \" + sha1 + \" already exists in the download queue.\");\n                    return;\n                }\n\n                MapDownloadQueue.Add(sha1);\n\n                if (MapDownloadQueue.Count == 1)\n                {\n                    object[] details = new object[3];\n                    details[0] = sha1;\n                    details[1] = myGame.ToLower();\n                    details[2] = mapName;\n\n                    ParameterizedThreadStart pts = new ParameterizedThreadStart(Download);\n                    Thread thread = new Thread(pts);\n                    thread.Start(details);\n                }\n            }\n        }\n\n        private static void Download(object details)\n        {\n            object[] sha1AndGame = (object[])details;\n            string sha1 = (string)sha1AndGame[0];\n            string myGameId = (string)sha1AndGame[1];\n            string mapName = (string)sha1AndGame[2];\n\n            Logger.Log(\"MapSharer: Preparing to download map \" + sha1 + \" with name: \" + mapName);\n\n            bool success;\n\n            try\n            {\n                Logger.Log(\"MapSharer: MapDownloadStarted\");\n                MapDownloadStarted?.Invoke(null, new SHA1EventArgs(sha1, mapName));\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"MapSharer: ERROR \" + ex.ToString());\n            }\n\n            string mapPath = DownloadMain(sha1, myGameId, mapName, out success);\n\n            lock (locker)\n            {\n                if (success)\n                {\n                    Logger.Log(\"MapSharer: Download of map \" + sha1 + \" completed succesfully.\");\n                    MapDownloadComplete?.Invoke(null, new SHA1EventArgs(sha1, mapName));\n                }\n                else\n                {\n                    Logger.Log(\"MapSharer: Download of map \" + sha1 + \"failed! Reason: \" + mapPath);\n                    MapDownloadFailed?.Invoke(null, new SHA1EventArgs(sha1, mapName));\n                }\n\n                MapDownloadQueue.Remove(sha1);\n\n                if (MapDownloadQueue.Count > 0)\n                {\n                    Logger.Log(\"MapSharer: Continuing custom map downloads.\");\n\n                    object[] array = new object[3];\n                    array[0] = MapDownloadQueue[0];\n                    array[1] = myGameId;\n                    array[2] = mapName;\n\n                    Download(array);\n                }\n            }\n        }\n\n        public static string GetMapFileName(string sha1, string mapName)\n            => mapName.ToWin32FileName() + \"_\" + sha1;\n\n        private static string DownloadMain(string sha1, string myGame, string mapName, out bool success)\n        {\n            try\n            {\n                string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, \"Maps\", \"Custom\");\n                string mapFileName = GetMapFileName(sha1, mapName);\n\n                FileInfo destinationFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($\"{mapFileName}.zip\"));\n\n                // This string is up here so we can check that there isn't already a .map file for this download.\n                // This prevents the client from crashing when trying to rename the unzipped file to a duplicate filename.\n                FileInfo newFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($\"{mapFileName}.{ClientConfiguration.Instance.MapFileExtension}\"));\n\n                try\n                {\n                    destinationFile.Delete();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"MapSharer: Failed to delete existing zip file: {ex.Message}\");\n                }\n\n                try\n                {\n                    newFile.Delete();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"MapSharer: Failed to delete existing map file: {ex.Message}\");\n                }\n\n                if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBDownloadURL))\n                {\n                    success = false;\n                    Logger.Log(\"MapSharer: Download URL is not configured.\");\n                    return null;\n                }\n\n                string url = string.Format(CultureInfo.InvariantCulture, \"{0}/{1}/{2}.zip\", ClientConfiguration.Instance.CnCNetMapDBDownloadURL, myGame, sha1);\n\n                try\n                {\n                    Logger.Log($\"MapSharer: Downloading URL: {url}\");\n                    new TimedHttpClient(DOWNLOAD_TIMEOUT).DownloadFile(url, destinationFile.FullName);\n                }\n                catch (Exception ex)\n                {\n                    success = false;\n                    return ex.Message;\n                }\n\n                destinationFile.Refresh();\n\n                if (!destinationFile.Exists)\n                {\n                    success = false;\n                    return null;\n                }\n\n                string extractedFile;\n\n                try\n                {\n                    extractedFile = ExtractZipFile(destinationFile.FullName, customMapsDirectory);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"MapSharer: Failed to extract map: {ex.Message}\");\n                    success = false;\n                    return ex.Message;\n                }\n\n                if (String.IsNullOrEmpty(extractedFile))\n                {\n                    success = false;\n                    return null;\n                }\n\n                try\n                {\n                    destinationFile.Delete();\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"MapSharer: Failed to delete zip file after extraction: {ex.Message}\");\n                }\n\n                success = true;\n                return extractedFile;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"MapSharer: Map download failed with exception: {ex.Message}\");\n                success = false;\n                return ex.Message;\n            }\n        }\n\n        class FileToUpload\n        {\n            public FileToUpload()\n            {\n                ContentType = \"application/octet-stream\";\n            }\n\n            public string Name { get; set; }\n            public string Filename { get; set; }\n            public string ContentType { get; set; }\n            public Stream Stream { get; set; }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/NameValidator.cs",
    "content": "﻿using System;\nusing System.Linq;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    public enum NameValidationError\n    {\n        None = 0,\n        EmptyName,\n        OffensiveName,\n        FirstCharacterIsNumber,\n        FirstCharacterIsHyphen,\n        InvalidCharacters,\n        TooLong\n    }\n\n    public static class NameValidator\n    {\n        /// <summary>\n        /// Gets the localized error message for a player name validation error.\n        /// </summary>\n        /// <param name=\"error\">The validation error.</param>\n        /// <returns>Localized error message, or null if the error is None.</returns>\n        public static string GetLocalizedPlayerNameErrorMessage(NameValidationError error)\n        {\n            switch (error)\n            {\n                case NameValidationError.None:\n                    return null;\n                case NameValidationError.EmptyName:\n                    return \"Please enter a name.\".L10N(\"Client:ClientCore:EnterAName\");\n                case NameValidationError.OffensiveName:\n                    return \"Please enter a name that is less offensive.\".L10N(\"Client:ClientCore:NameOffensive\");\n                case NameValidationError.FirstCharacterIsNumber:\n                    return \"The first character in the player name cannot be a number.\".L10N(\"Client:ClientCore:NameFirstIsNumber\");\n                case NameValidationError.FirstCharacterIsHyphen:\n                    return \"The first character in the player name cannot be a hyphen ( - ).\".L10N(\"Client:ClientCore:NameFirstIsHyphen\");\n                case NameValidationError.InvalidCharacters:\n                    return \"Your player name has invalid characters in it.\".L10N(\"Client:ClientCore:NameInvalidChar1\") + Environment.NewLine +\n                           \"Allowed characters are anything from A to Z and numbers.\".L10N(\"Client:ClientCore:NameInvalidChar2\");\n                case NameValidationError.TooLong:\n                    return \"Your nickname is too long.\".L10N(\"Client:ClientCore:NameTooLong\");\n                default:\n                    return null;\n            }\n        }\n\n        /// <summary>\n        /// Gets the localized error message for a game name validation error.\n        /// </summary>\n        /// <param name=\"error\">The validation error.</param>\n        /// <returns>Localized error message, or null if the error is None.</returns>\n        public static string GetLocalizedGameNameErrorMessage(NameValidationError error)\n        {\n            switch (error)\n            {\n                case NameValidationError.None:\n                    return null;\n                case NameValidationError.EmptyName:\n                    return \"Please enter a game name.\".L10N(\"Client:Main:PleaseEnterGameName\");\n                case NameValidationError.OffensiveName:\n                    return \"Please enter a less offensive game name.\".L10N(\"Client:Main:GameNameOffensiveText\");\n                default:\n                    return null;\n            }\n        }\n\n        /// <summary>\n        /// Checks if the player's nickname is valid for CnCNet.\n        /// </summary>\n        /// <param name=\"name\">The player name to validate.</param>\n        /// <param name=\"localizedErrorMessage\">The localized error message if validation fails, otherwise null.</param>\n        /// <returns>NameValidationError.None if the nickname is valid, otherwise the specific validation error.</returns>\n        public static NameValidationError IsNameValid(string name, out string localizedErrorMessage)\n        {\n            var profanityFilter = new ProfanityFilter();\n\n            if (string.IsNullOrEmpty(name))\n            {\n                localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.EmptyName);\n                return NameValidationError.EmptyName;\n            }\n\n            if (profanityFilter.IsOffensive(name))\n            {\n                localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.OffensiveName);\n                return NameValidationError.OffensiveName;\n            }\n\n            if (int.TryParse(name.Substring(0, 1), out _))\n            {\n                localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.FirstCharacterIsNumber);\n                return NameValidationError.FirstCharacterIsNumber;\n            }\n\n            if (name[0] == '-')\n            {\n                localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.FirstCharacterIsHyphen);\n                return NameValidationError.FirstCharacterIsHyphen;\n            }\n\n            // Check that there are no invalid chars\n            char[] allowedCharacters = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_[]|\\\\{}^`\".ToCharArray();\n            char[] nicknameChars = name.ToCharArray();\n\n            foreach (char nickChar in nicknameChars)\n            {\n                if (!allowedCharacters.Contains(nickChar))\n                {\n                    localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.InvalidCharacters);\n                    return NameValidationError.InvalidCharacters;\n                }\n            }\n\n            if (name.Length > ClientConfiguration.Instance.MaxNameLength)\n            {\n                localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.TooLong);\n                return NameValidationError.TooLong;\n            }\n\n            localizedErrorMessage = null;\n            return NameValidationError.None;\n        }\n\n        /// <summary>\n        /// Returns player nickname constrained to maximum allowed length and with invalid characters for offline nicknames removed.\n        /// Does not check for offensive words or invalid characters for CnCNet.\n        /// </summary>\n        /// <param name=\"name\">Player nickname.</param>\n        /// <returns>Player nickname with invalid offline nickname characters removed and constrained to maximum name length.</returns>\n        public static string GetValidOfflineName(string name)\n        {\n            char[] disallowedCharacters = \",;\".ToCharArray();\n\n            string validName = new string(name.Trim().Where(c => !disallowedCharacters.Contains(c)).ToArray());\n\n            if (validName.Length > ClientConfiguration.Instance.MaxNameLength)\n                return validName.Substring(0, ClientConfiguration.Instance.MaxNameLength);\n\n            return validName;\n        }\n\n        /// <summary>\n        /// Checks if a lobby room name is valid.\n        /// </summary>\n        /// <param name=\"name\">The lobby name to validate.</param>\n        /// <param name=\"localizedErrorMessage\">The localized error message if validation fails, otherwise null.</param>\n        /// <returns>NameValidationError.None if the name is valid, otherwise the specific validation error.</returns>\n        public static NameValidationError IsGameNameValid(string name, out string localizedErrorMessage)\n        {\n            var profanityFilter = new ProfanityFilter();\n\n            if (string.IsNullOrEmpty(name))\n            {\n                localizedErrorMessage = GetLocalizedGameNameErrorMessage(NameValidationError.EmptyName);\n                return NameValidationError.EmptyName;\n            }\n\n            if (profanityFilter.IsOffensive(name))\n            {\n                localizedErrorMessage = GetLocalizedGameNameErrorMessage(NameValidationError.OffensiveName);\n                return NameValidationError.OffensiveName;\n            }\n\n            localizedErrorMessage = null;\n            return NameValidationError.None;\n        }\n\n        /// <summary>\n        /// Sanitizes a lobby name by removing invalid characters.\n        /// </summary>\n        /// <param name=\"name\">The lobby room name to sanitize.</param>\n        /// <returns>The sanitized lobby room name.</returns>\n        public static string GetSanitizedGameName(string name)\n        {\n            // semicolons are used as separators in the protocol\n            return name.Replace(\";\", string.Empty).Trim();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/SHA1EventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    public class SHA1EventArgs : EventArgs\n    {\n        public SHA1EventArgs(string sha1, string mapName)\n        {\n            SHA1 = sha1;\n            MapName = mapName;\n        }\n\n        public string SHA1 { get; private set; }\n\n        public string MapName { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/TimedHttpClient.cs",
    "content": "#nullable enable\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    /// <summary>\n    /// An HTTP client wrapper that enforces a per-request timeout covering\n    /// the entire operation, including both connection establishment and\n    /// response body read.\n    /// </summary>\n    internal sealed class TimedHttpClient\n    {\n        private static readonly HttpClient sharedHttpClient = new HttpClient()\n        {\n            Timeout = Timeout.InfiniteTimeSpan\n        };\n\n        private readonly int timeoutMilliseconds;\n\n        /// <param name=\"timeoutMilliseconds\">\n        /// The timeout in milliseconds. The entire HTTP operation must complete within this time.\n        /// </param>\n        public TimedHttpClient(int timeoutMilliseconds)\n        {\n            this.timeoutMilliseconds = timeoutMilliseconds;\n        }\n\n        /// <summary>\n        /// Downloads the resource at the specified URL as a string.\n        /// Guaranteed to return or throw within the configured timeout.\n        /// </summary>\n        public async Task<string> GetStringAsync(string url)\n        {\n            using var cts = new CancellationTokenSource(timeoutMilliseconds);\n\n            // GetAsync with the default HttpCompletionOption.ResponseContentRead downloads\n            // the complete response body before completing, so cts.Token covers the full operation.\n            using var response = await sharedHttpClient.GetAsync(url, cts.Token).ConfigureAwait(false);\n            response.EnsureSuccessStatusCode();\n\n            return await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Synchronous wrapper for <see cref=\"GetStringAsync\"/>.\n        /// </summary>\n        public string GetString(string url)\n            => GetStringAsync(url).GetAwaiter().GetResult();\n\n        /// <summary>\n        /// Downloads the resource at the specified URL as a byte array.\n        /// Guaranteed to return or throw within the configured timeout.\n        /// </summary>\n        public async Task<byte[]> GetBytesAsync(string url)\n        {\n            using var cts = new CancellationTokenSource(timeoutMilliseconds);\n\n            using var response = await sharedHttpClient.GetAsync(url, cts.Token).ConfigureAwait(false);\n            response.EnsureSuccessStatusCode();\n\n            return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Synchronous wrapper for <see cref=\"GetBytesAsync\"/>.\n        /// </summary>\n        public byte[] GetBytes(string url)\n            => GetBytesAsync(url).GetAwaiter().GetResult();\n\n        /// <summary>\n        /// Downloads the resource at the specified URL and saves it to a file.\n        /// Guaranteed to complete or throw within the configured timeout.\n        /// </summary>\n        public async Task DownloadFileAsync(string url, string filePath)\n        {\n            using var cts = new CancellationTokenSource(timeoutMilliseconds);\n\n            // Use ResponseHeadersRead for streaming to avoid buffering the entire file in memory.\n            using var response = await sharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false);\n            response.EnsureSuccessStatusCode();\n\n            using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);\n            using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);\n\n            // CopyToAsync respects the cancellation token between iterations,\n            // ensuring the timeout is enforced throughout the body read.\n            await contentStream.CopyToAsync(fileStream, 81920, cts.Token).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Synchronous wrapper for <see cref=\"DownloadFileAsync\"/>.\n        /// </summary>\n        public void DownloadFile(string url, string filePath)\n            => DownloadFileAsync(url, filePath).GetAwaiter().GetResult();\n\n        /// <summary>\n        /// Posts the specified content to the URL and returns the response body as a byte array.\n        /// Guaranteed to return or throw within the configured timeout.\n        /// </summary>\n        public async Task<byte[]> PostAsync(string url, HttpContent content)\n        {\n            using var cts = new CancellationTokenSource(timeoutMilliseconds);\n\n            using var response = await sharedHttpClient.PostAsync(url, content, cts.Token).ConfigureAwait(false);\n            response.EnsureSuccessStatusCode();\n\n            return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Synchronous wrapper for <see cref=\"PostAsync\"/>.\n        /// </summary>\n        public byte[] Post(string url, HttpContent content)\n            => PostAsync(url, content).GetAwaiter().GetResult();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Online;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\nusing System.Linq;\n\nnamespace DTAClient.Domain.Multiplayer.CnCNet\n{\n    public class TunnelHandler : GameComponent\n    {\n        /// <summary>\n        /// Determines the time between pinging the current tunnel (if it's set).\n        /// </summary>\n        private const double CURRENT_TUNNEL_PING_INTERVAL = 20.0;\n\n        /// <summary>\n        /// A reciprocal to the value which determines how frequent the full tunnel\n        /// refresh would be done instead of just pinging the current tunnel (1/N of \n        /// current tunnel ping refreshes would be substituted by a full list refresh).\n        /// Multiply by <see cref=\"CURRENT_TUNNEL_PING_INTERVAL\"/> to get the interval \n        /// between full list refreshes.\n        /// </summary>\n        private const uint CYCLES_PER_TUNNEL_LIST_REFRESH = 6;\n\n        private const int SUPPORTED_TUNNEL_VERSION = 2;\n\n        private readonly object _refreshLock = new object();\n        private bool _refreshInProgress = false;\n\n        public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game)\n        {\n            this.wm = wm;\n            this.connectionManager = connectionManager;\n\n            wm.Game.Components.Add(this);\n\n            Enabled = false;\n\n            connectionManager.Connected += ConnectionManager_Connected;\n            connectionManager.Disconnected += ConnectionManager_Disconnected;\n            connectionManager.ConnectionLost += ConnectionManager_ConnectionLost;\n        }\n\n        public List<CnCNetTunnel> Tunnels { get; private set; } = new List<CnCNetTunnel>();\n        public CnCNetTunnel CurrentTunnel { get; set; } = null;\n\n        public event EventHandler TunnelsRefreshed;\n        public event EventHandler CurrentTunnelPinged;\n        public event Action<int> TunnelPinged;\n\n        private WindowManager wm;\n        private CnCNetManager connectionManager;\n\n        private TimeSpan timeSinceTunnelRefresh = TimeSpan.MaxValue;\n        private uint skipCount = 0;\n\n        private void DoTunnelPinged(int index)\n        {\n            if (TunnelPinged != null)\n                wm.AddCallback(TunnelPinged, index);\n        }\n\n        private void DoCurrentTunnelPinged()\n        {\n            if (CurrentTunnelPinged != null)\n                wm.AddCallback(CurrentTunnelPinged, this, EventArgs.Empty);\n        }\n\n        private void ConnectionManager_Connected(object sender, EventArgs e) => Enabled = true;\n\n        private void ConnectionManager_ConnectionLost(object sender, Online.EventArguments.ConnectionLostEventArgs e) => Enabled = false;\n\n        private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false;\n\n        private void RefreshTunnelsAsync()\n        {\n            lock (_refreshLock)\n            {\n                if (_refreshInProgress)\n                    return;\n                _refreshInProgress = true;\n            }\n\n            Task.Run(() =>\n            {\n                try\n                {\n                    List<CnCNetTunnel> tunnels = RefreshTunnels();\n                    wm.AddCallback(new Action<List<CnCNetTunnel>>(HandleRefreshedTunnels), tunnels);\n                }\n                finally\n                {\n                    lock (_refreshLock)\n                    {\n                        _refreshInProgress = false;\n                    }\n                }\n            });\n        }\n\n        private void HandleRefreshedTunnels(List<CnCNetTunnel> newTunnels)\n        {\n            if (newTunnels.Count == 0)\n            {\n                TunnelsRefreshed?.Invoke(this, EventArgs.Empty);\n                return;\n            }\n\n            var existingTunnels = Tunnels.ToDictionary(t => $\"{t.Address}:{t.Port}\");\n            var updatedTunnels = new List<CnCNetTunnel>();\n\n            foreach (var newTunnel in newTunnels)\n            {\n                string key = $\"{newTunnel.Address}:{newTunnel.Port}\";\n                if (existingTunnels.TryGetValue(key, out var existingTunnel))\n                {\n                    // update existing tunnels\n                    existingTunnel.UpdateFrom(newTunnel);\n                    updatedTunnels.Add(existingTunnel);\n                }\n                else\n                {\n                    // add new tunnels\n                    updatedTunnels.Add(newTunnel);\n                }\n            }\n\n            // remove old tunnels\n            Tunnels = updatedTunnels;\n            TunnelsRefreshed?.Invoke(this, EventArgs.Empty);\n\n            for (int i = 0; i < Tunnels.Count; i++)\n            {\n                if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended)\n                    _ = PingListTunnelAsync(i);\n            }\n\n            if (CurrentTunnel != null)\n            {\n                var updatedTunnel = Tunnels.Find(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port);\n                if (updatedTunnel != null)\n                {\n                    // don't re-ping if the tunnel still exists in list, just update the tunnel instance and\n                    // fire the event handler (the tunnel was already pinged when traversing the tunnel list)\n                    CurrentTunnel = updatedTunnel;\n                    DoCurrentTunnelPinged();\n                }\n                else\n                {\n                    // tunnel is not in the list anymore so it's not updated with a list instance and pinged\n                    PingCurrentTunnelAsync();\n                }\n            }\n        }\n\n        private Task PingListTunnelAsync(int index)\n        {\n            return Task.Run(() =>\n            {\n                Tunnels[index].UpdatePing();\n                DoTunnelPinged(index);\n            });\n        }\n\n        private Task PingCurrentTunnelAsync(bool checkTunnelList = false)\n        {\n            return Task.Run(() =>\n            {\n                var tunnel = CurrentTunnel;\n                if (tunnel == null) return;\n\n                tunnel.UpdatePing();\n                DoCurrentTunnelPinged();\n\n                if (checkTunnelList)\n                {\n                    int tunnelIndex = Tunnels.FindIndex(t => t.Address == tunnel.Address && t.Port == tunnel.Port);\n                    if (tunnelIndex > -1)\n                        DoTunnelPinged(tunnelIndex);\n                }\n            });\n        }\n\n        private bool OnlineTunnelDataAvailable => !string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetTunnelListURL);\n        private bool OfflineTunnelDataAvailable => SafePath.GetFile(ProgramConstants.ClientUserFilesPath, \"tunnel_cache\").Exists;\n\n        private byte[] GetRawTunnelDataOnline()\n        {\n            return new TimedHttpClient(10000).GetBytes(ClientConfiguration.Instance.CnCNetTunnelListURL);\n        }\n\n        private byte[] GetRawTunnelDataOffline()\n        {\n            FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, \"tunnel_cache\");\n            return File.ReadAllBytes(tunnelCacheFile.FullName);\n        }\n\n        private byte[] GetRawTunnelData(int retryCount = 2)\n        {\n            Logger.Log(\"Fetching tunnel server info.\");\n\n            if (OnlineTunnelDataAvailable)\n            {\n                for (int i = 0; i < retryCount; i++)\n                {\n                    try\n                    {\n                        byte[] data = GetRawTunnelDataOnline();\n                        return data;\n                    }\n                    catch (Exception ex)\n                    {\n                        Logger.Log(\"Error when downloading tunnel server info: \" + ex.Message);\n                        if (i < retryCount - 1)\n                            Logger.Log(\"Retrying.\");\n                        else\n                            Logger.Log(\"Fetching tunnel server list failed.\");\n                    }\n                }\n            }\n            else\n            {\n                // Don't fetch the latest tunnel list if it is explicitly disabled\n                // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship,\n                // where players might either establish a substitute server or manually distribute the tunnel cache file\n                Logger.Log(\"Fetching tunnel server list online is disabled.\");\n            }\n\n            if (OfflineTunnelDataAvailable)\n            {\n                Logger.Log(\"Using cached tunnel data.\");\n                byte[] data = GetRawTunnelDataOffline();\n                return data;\n            }\n            else\n                Logger.Log(\"Tunnel cache file doesn't exist!\");\n\n            return null;\n        }\n\n\n        /// <summary>\n        /// Downloads and parses the list of CnCNet tunnels.\n        /// </summary>\n        /// <returns>A list of tunnel servers.</returns>\n        private List<CnCNetTunnel> RefreshTunnels()\n        {\n            List<CnCNetTunnel> returnValue = new List<CnCNetTunnel>();\n            var seenAddresses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n            FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, \"tunnel_cache\");\n\n            byte[] data = GetRawTunnelData();\n            if (data is null)\n                return returnValue;\n\n            string convertedData = Encoding.Default.GetString(data);\n\n            string[] serverList = convertedData.Split(new string[] { \"\\r\\n\", \"\\n\" }, StringSplitOptions.RemoveEmptyEntries);\n\n            // skip first header item (\"address;country;countrycode;name;password;clients;maxclients;official;latitude;longitude;version;distance\")\n            foreach (string serverInfo in serverList.Skip(1))\n            {\n                try\n                {\n                    CnCNetTunnel tunnel = CnCNetTunnel.Parse(serverInfo);\n\n                    if (tunnel == null)\n                        continue;\n\n                    if (tunnel.RequiresPassword)\n                        continue;\n\n                    if (tunnel.Version != SUPPORTED_TUNNEL_VERSION)\n                        continue;\n\n                    if (!seenAddresses.Add($\"{tunnel.Address}:{tunnel.Port}\"))\n                        continue;\n\n                    returnValue.Add(tunnel);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Caught an exception when parsing a tunnel server: \" + ex.ToString());\n                }\n            }\n\n            if (returnValue.Count > 0)\n            {\n                try\n                {\n                    if (tunnelCacheFile.Exists)\n                        tunnelCacheFile.Delete();\n\n                    DirectoryInfo clientDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath);\n\n                    if (!clientDirectoryInfo.Exists)\n                        clientDirectoryInfo.Create();\n\n                    File.WriteAllBytes(tunnelCacheFile.FullName, data);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Refreshing tunnel cache file failed! Returned error: \" + ex.ToString());\n                }\n            }\n\n            Logger.Log($\"Successfully refreshed tunnel cache with {returnValue.Count} servers.\");\n            return returnValue;\n        }\n\n        public override void Update(GameTime gameTime)\n        {\n            if (timeSinceTunnelRefresh > TimeSpan.FromSeconds(CURRENT_TUNNEL_PING_INTERVAL))\n            {\n                if (skipCount % CYCLES_PER_TUNNEL_LIST_REFRESH == 0)\n                {\n                    skipCount = 0;\n                    RefreshTunnelsAsync();\n                }\n                else if (CurrentTunnel != null)\n                {\n                    PingCurrentTunnelAsync(true);\n                }\n\n                timeSinceTunnelRefresh = TimeSpan.Zero;\n                skipCount++;\n            }\n            else\n                timeSinceTunnelRefresh += gameTime.ElapsedGameTime;\n\n            base.Update(gameTime);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs",
    "content": "﻿using Rampastring.Tools;\nusing System.Collections.Generic;\nusing System;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// Holds information about enemy houses in a co-op map.\n    /// </summary>\n    public struct CoopHouseInfo\n    {\n        public CoopHouseInfo(int side, int color, int startingLocation)\n        {\n            Side = side;\n            Color = color;\n            StartingLocation = startingLocation;\n        }\n\n        /// <summary>\n        /// The index of the enemy house's side.\n        /// </summary>\n        public int Side;\n\n        /// <summary>\n        /// The index of the enemy house's color.\n        /// </summary>\n        public int Color;\n\n        /// <summary>\n        /// The starting location waypoint of the enemy house.\n        /// </summary>\n        public int StartingLocation;\n\n        public static List<CoopHouseInfo> GetGenericHouseInfoList(IniSection iniSection, string keyName)\n        {\n            var houseList = new List<CoopHouseInfo>();\n\n            for (int i = 0; ; i++)\n            {\n                string[] houseInfo = iniSection.GetStringValue(keyName + i, string.Empty).Split(\n                    new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);\n\n                if (houseInfo.Length == 0)\n                    break;\n\n                int[] info = Conversions.IntArrayFromStringArray(houseInfo);\n                var chInfo = new CoopHouseInfo(info[0], info[1], info[2]);\n\n                houseList.Add(new CoopHouseInfo(info[0], info[1], info[2]));\n            }\n\n            return houseList;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CoopMapInfo.cs",
    "content": "﻿#nullable enable\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class CoopMapInfo\n    {\n        [JsonInclude]\n        public List<CoopHouseInfo> EnemyHouses = new List<CoopHouseInfo>();\n\n        [JsonInclude]\n        public List<CoopHouseInfo> AllyHouses = new List<CoopHouseInfo>();\n\n        [JsonInclude]\n        public List<int> DisallowedPlayerSides = new List<int>();\n\n        [JsonInclude]\n        public List<int> DisallowedPlayerColors = new List<int>();\n\n        public CoopMapInfo() { }\n\n        public void Initialize(IniSection section)\n        {\n            DisallowedPlayerSides = section.GetListValue(\"DisallowedPlayerSides\", ',', int.Parse);\n            DisallowedPlayerColors = section.GetListValue(\"DisallowedPlayerColors\", ',', int.Parse);\n            EnemyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, \"EnemyHouse\");\n            AllyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, \"AllyHouse\");\n        }\n\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/CustomMapCache.cs",
    "content": "﻿using System;\nusing System.Collections.Concurrent;\nusing System.Diagnostics.CodeAnalysis;\nusing System.IO;\nusing System.Text.Json.Serialization;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class CustomMapCache\n    {\n        [JsonInclude]\n        [JsonPropertyName(\"version\")]\n        public required int Version { get; set; }\n\n        [JsonInclude]\n        [JsonPropertyName(\"maps\")]\n        public required ConcurrentDictionary<string, Item> Items { get; set; }\n\n        public record Item\n        {\n            [JsonInclude]\n            public required Map Map { get; init; }\n\n            [JsonInclude]\n            public long FileSize { get; private set; }\n\n            [JsonInclude]\n            public DateTime LastWriteTimeUtc { get; private set; }\n\n            public Item() : base() { }\n\n            [SetsRequiredMembers]\n            public Item(Map map)\n            {\n                Map = map;\n\n                FileInfo fileInfo = new(Map.CompleteFilePath);\n                if (fileInfo.Exists)\n                {\n                    FileSize = fileInfo.Length;\n                    LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;\n                }\n                else\n                {\n                    FileSize = 0;\n                    LastWriteTimeUtc = DateTime.MinValue;\n                }\n            }\n\n            public bool IsOutdated()\n            {\n                Item refreshedItem = new(Map);\n                return refreshedItem.FileSize != FileSize || refreshedItem.LastWriteTimeUtc != LastWriteTimeUtc;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GameMode.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A multiplayer game mode.\n    /// </summary>\n    public class GameMode : GameModeMapBase\n    {\n        public GameMode(string name)\n        {\n            Name = name;\n            Initialize();\n        }\n\n        private const string BASE_INI_PATH = \"INI/Map Code/\";\n        private const string SPAWN_INI_OPTIONS_SECTION = \"ForcedSpawnIniOptions\";\n\n        /// <summary>\n        /// The internal (INI) name of the game mode.\n        /// </summary>\n        public string Name { get; set; }\n\n        /// <summary>\n        /// The user-interface name of the game mode.\n        /// </summary>\n        public string UIName { get; private set; }\n\n        /// <summary>\n        /// The original user-interface name of the game mode before translation.\n        /// </summary>\n        public string UntranslatedUIName { get; private set; }\n\n        /// <summary>\n        /// List of side indices players cannot select in this game mode.\n        /// </summary>\n        public List<int> DisallowedPlayerSides = new List<int>();\n\n        /// <summary>\n        /// List of side indices human players cannot select in this game mode.\n        /// </summary>\n        public List<int> DisallowedHumanPlayerSides = new List<int>();\n\n        /// <summary>\n        /// List of side indices computer players cannot select in this game mode.\n        /// </summary>\n        public List<int> DisallowedComputerPlayerSides = new List<int>();\n\n        /// </summary>\n        /// Override for minimum amount of players needed to play any map in this game mode.\n        /// Priority sequences: GameMode.MinPlayersOverride, Map.MinPlayers, GameMode.MinPlayers.\n        /// </summary>\n        public int? MinPlayersOverride { get; private set; }\n\n        public int? MaxPlayersOverride { get; private set; }\n\n        private string mapCodeININame;\n        private List<string> randomizedMapCodeININames;\n        private int randomizedMapCodesCount;\n\n        private string forcedOptionsSection;\n\n        public List<Map> Maps = new List<Map>();\n\n        public List<KeyValuePair<string, bool>> ForcedCheckBoxValues = new List<KeyValuePair<string, bool>>();\n        public List<KeyValuePair<string, int>> ForcedDropDownValues = new List<KeyValuePair<string, int>>();\n\n        private List<KeyValuePair<string, string>> ForcedSpawnIniOptions = new List<KeyValuePair<string, string>>();\n\n        public void Initialize()\n        {\n            IniFile forcedOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath));\n            IniSection section = forcedOptionsIni.GetSection(Name) ?? new IniSection(Name);\n\n            UntranslatedUIName = section.GetStringValue(\"UIName\", Name);\n            UIName = UntranslatedUIName.L10N($\"INI:GameModes:{Name}:UIName\");\n\n            InitializeBaseSettingsFromIniSection(section, isCustomMap: false);\n\n            MinPlayersOverride = section.GetIntValueOrNull(\"MinPlayersOverride\");\n            MaxPlayersOverride = section.GetIntValueOrNull(\"MaxPlayersOverride\");\n\n            forcedOptionsSection = section.GetStringValue(\"ForcedOptions\", string.Empty);\n            mapCodeININame = section.GetStringValue(\"MapCodeININame\", Name + \".ini\");\n            randomizedMapCodeININames = section.GetStringValue(\"RandomizedMapCodeININames\", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();\n            randomizedMapCodesCount = section.GetIntValue(\"RandomizedMapCodesCount\", 1);\n\n            DisallowedPlayerSides = section.GetListValue(\"DisallowedPlayerSides\", ',', int.Parse);\n            DisallowedHumanPlayerSides = section.GetListValue(\"DisallowedHumanPlayerSides\", ',', int.Parse);\n            DisallowedComputerPlayerSides = section.GetListValue(\"DisallowedComputerPlayerSides\", ',', int.Parse);\n\n            ParseForcedOptions(forcedOptionsIni);\n\n            ParseSpawnIniOptions(forcedOptionsIni);\n        }\n\n        private void ParseForcedOptions(IniFile forcedOptionsIni)\n        {\n            if (string.IsNullOrEmpty(forcedOptionsSection))\n                return;\n\n            List<string> keys = forcedOptionsIni.GetSectionKeys(forcedOptionsSection);\n\n            if (keys == null)\n                return;\n\n            foreach (string key in keys)\n            {\n                string value = forcedOptionsIni.GetStringValue(forcedOptionsSection, key, string.Empty);\n\n                int intValue = 0;\n                if (int.TryParse(value, out intValue))\n                {\n                    ForcedDropDownValues.Add(new KeyValuePair<string, int>(key, intValue));\n                }\n                else\n                {\n                    ForcedCheckBoxValues.Add(new KeyValuePair<string, bool>(key, Conversions.BooleanFromString(value, false)));\n                }\n            }\n        }\n\n        private void ParseSpawnIniOptions(IniFile forcedOptionsIni)\n        {\n            string section = forcedOptionsIni.GetStringValue(Name, \"ForcedSpawnIniOptions\", Name + SPAWN_INI_OPTIONS_SECTION);\n\n            List<string> spawnIniKeys = forcedOptionsIni.GetSectionKeys(section);\n\n            if (spawnIniKeys == null)\n                return;\n\n            foreach (string key in spawnIniKeys)\n            {\n                ForcedSpawnIniOptions.Add(new KeyValuePair<string, string>(key,\n                    forcedOptionsIni.GetStringValue(section, key, string.Empty)));\n            }\n        }\n\n        public void ApplySpawnIniCode(IniFile spawnIni)\n        {\n            foreach (KeyValuePair<string, string> key in ForcedSpawnIniOptions)\n                spawnIni.SetStringValue(\"Settings\", key.Key, key.Value);\n        }\n\n        public List<IniFile> GetMapRulesIniFiles(Random pseudoRandom)\n        {\n            var mapRules = new List<IniFile>() { new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, mapCodeININame)) };\n            if (randomizedMapCodeININames.Count == 0)\n                return mapRules;\n\n            Dictionary<string, int> randomOrder = new();\n            foreach (string name in randomizedMapCodeININames)\n            {\n                randomOrder[name] = pseudoRandom.Next();\n            }\n\n            mapRules.AddRange(\n                from iniName in randomizedMapCodeININames.OrderBy(x => randomOrder[x]).Take(randomizedMapCodesCount)\n                select new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, iniName)));\n\n            return mapRules;\n        }\n\n        protected bool Equals(GameMode other) => string.Equals(Name, other?.Name, StringComparison.InvariantCultureIgnoreCase);\n\n        public override int GetHashCode() => (Name != null ? Name.GetHashCode() : 0);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GameModeMap.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\n\nusing ClientCore.Extensions;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// An instance of a Map in a given GameMode\n    /// </summary>\n    public record GameModeMap : IGameModeMap\n    {\n        public required GameMode GameMode { get; init; }\n        public required Map Map { get; init; }\n        public bool IsFavorite { get; set; } = false;\n        public GameModeMap() { }\n\n        [SetsRequiredMembers]\n        public GameModeMap(GameMode gameMode, Map map)\n        {\n            GameMode = gameMode;\n            Map = map;\n        }\n\n        [SetsRequiredMembers]\n        public GameModeMap(GameMode gameMode, Map map, bool isFavorite)\n        {\n            GameMode = gameMode;\n            Map = map;\n            IsFavorite = isFavorite;\n        }\n\n        public string ToUntranslatedUIString() => $\"{Map.UntranslatedName} - {GameMode.UntranslatedUIName}\";\n\n        public string ToUIString() => $\"{Map.Name} - {GameMode.UIName}\";\n\n        public override string ToString() => ToUIString();\n\n        public List<int> AllowedStartingLocations\n        {\n            get\n            {\n                var ret = Map.AllowedStartingLocations ?? GameMode.AllowedStartingLocations ?? Enumerable.Range(1, MaxPlayers).ToList();\n\n                if (ret.Count != MaxPlayers)\n                    throw new Exception(string.Format(\"The number of AllowedStartingLocations does not equal to MaxPlayer.\".L10N(\"Client:Main:InvalidAllowedStartingLocationsCount\")));\n\n                return ret;\n            }\n        }\n\n        public int CoopDifficultyLevel =>\n            Map.CoopDifficultyLevel ?? GameMode.CoopDifficultyLevel ?? 0;\n\n        public CoopMapInfo? CoopInfo =>\n            Map.CoopInfo ?? GameMode.CoopInfo ?? null;\n\n        public bool EnforceMaxPlayers =>\n            Map.EnforceMaxPlayers ?? GameMode.EnforceMaxPlayers ?? false;\n\n        public bool ForceNoTeams =>\n            Map.ForceNoTeams ?? GameMode.ForceNoTeams ?? false;\n\n        public bool ForceRandomStartLocations =>\n            Map.ForceRandomStartLocations ?? GameMode.ForceRandomStartLocations ?? false;\n\n        public bool HumanPlayersOnly =>\n            Map.HumanPlayersOnly ?? GameMode.HumanPlayersOnly ?? false;\n\n        public bool IsCoop =>\n            Map.IsCoop ?? GameMode.IsCoop ?? false;\n\n        public int MaxPlayers =>\n            // Note: GameLobbyBase.GetMapList() assumes the priority.\n            // If you have modified the expression here, you should also update GameLobbyBase.GetMapList().\n            GameMode.MaxPlayersOverride ?? Map.MaxPlayers ?? GameMode.MaxPlayers ?? 0;\n\n        public int MinPlayers =>\n            GameMode.MinPlayersOverride ?? Map.MinPlayers ?? GameMode.MinPlayers ?? 0;\n\n        public bool MultiplayerOnly =>\n            Map.MultiplayerOnly ?? GameMode.MultiplayerOnly ?? false;\n\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GameModeMapBase.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Text.Json.Serialization;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public abstract class GameModeMapBase\n    {\n        public const int MAX_PLAYERS = 8;\n\n        /// <summary>\n        /// The maximum amount of players supported by the map or a game mode (such as a 2v2 mode).\n        /// </summary>\n        [JsonInclude]\n        public int? MaxPlayers { get; private set; }\n\n        /// <summary>\n        /// The minimum amount of players supported by the map or a game mode.\n        /// </summary>\n        [JsonInclude]\n        public int? MinPlayers { get; private set; }\n\n        /// <summary>\n        /// Whether to use MaxPlayers for limiting the player count of the map or a game mode.\n        /// If false (which is the default), MaxPlayers is only used for randomizing\n        /// players to starting waypoints.\n        /// </summary>\n        [JsonInclude]\n        public bool? EnforceMaxPlayers { get; private set; }\n\n        /// <summary>\n        /// The allowed starting locations for this map or game mode.\n        /// </summary>\n        [JsonInclude]\n        public List<int>? AllowedStartingLocations { get; private set; }\n\n        /// <summary>\n        /// Controls if the map is meant for a co-operation game mode\n        /// (enables briefing logic and forcing options, among others).\n        /// </summary>\n        [JsonInclude]\n        public bool? IsCoop { get; private set; }\n\n        /// <summary>\n        /// Contains co-op information.\n        /// </summary>\n        [JsonInclude]\n        public CoopMapInfo? CoopInfo { get; private set; }\n\n        [JsonInclude]\n        public int? CoopDifficultyLevel { get; set; }\n\n        /// <summary>\n        /// If set, this map cannot be played on Skirmish.\n        /// </summary>\n        [JsonInclude]\n        public bool? MultiplayerOnly { get; private set; }\n\n        /// <summary>\n        /// If set, this map cannot be played with AI players.\n        /// </summary>\n        [JsonInclude]\n        public bool? HumanPlayersOnly { get; private set; }\n\n        /// <summary>\n        /// If set, players are forced to random starting locations on this map.\n        /// </summary>\n        [JsonInclude]\n        public bool? ForceRandomStartLocations { get; private set; }\n\n        /// <summary>\n        /// If set, players are forced to different teams on this map.\n        /// </summary>\n        [JsonInclude]\n        public bool? ForceNoTeams { get; private set; }\n\n        protected void InitializeBaseSettingsFromIniSection(IniSection section, bool isCustomMap)\n        {\n            // MinPlayers\n            MinPlayers = section.GetIntValueOrNull(isCustomMap ? \"MinPlayer\" : \"MinPlayers\");\n\n            // MaxPlayers\n            if (isCustomMap)\n                MaxPlayers = section.GetIntValueOrNull(\"ClientMaxPlayer\") ?? section.GetIntValueOrNull(\"MaxPlayer\");\n            else\n                MaxPlayers = section.GetIntValueOrNull(\"MaxPlayers\");\n\n            // EnforceMaxPlayers\n            EnforceMaxPlayers = section.GetBooleanValueOrNull(\"EnforceMaxPlayers\");\n\n            // AllowedStartingLocations\n            List<int>? rawAllowedStartingLocations = section.GetListValueOrNull<int>(\"AllowedStartingLocations\", ',', int.Parse);\n\n            if (rawAllowedStartingLocations != null && rawAllowedStartingLocations.Count > 0)\n            {\n                // In configuration files, the number starts from 0. While in the code, the number starts from 1.\n                AllowedStartingLocations = rawAllowedStartingLocations.Select(x => x + 1).Distinct().OrderBy(x => x).ToList();\n\n                if (AllowedStartingLocations.Max() > MAX_PLAYERS || AllowedStartingLocations.Min() <= 0)\n                    throw new Exception(string.Format(\"Invalid AllowedStartingLocations {0}\".L10N(\"Client:Main:InvalidAllowedStartingLocations\"), string.Join(\", \", rawAllowedStartingLocations)));\n            }\n\n            // IsCoop\n            IsCoop = section.GetBooleanValueOrNull(\"IsCoopMission\");\n\n            // CoopInfo\n            if (IsCoop ?? false)\n            {\n                CoopInfo = new CoopMapInfo();\n                CoopInfo.Initialize(section);\n            }\n\n            // MultiplayerOnly\n            MultiplayerOnly = section.GetBooleanValueOrNull(isCustomMap ? \"ClientMultiplayerOnly\" : \"MultiplayerOnly\");\n\n            // HumanPlayersOnly\n            HumanPlayersOnly = section.GetBooleanValueOrNull(\"HumanPlayersOnly\");\n\n            // ForceRandomStartLocations\n            ForceRandomStartLocations = section.GetBooleanValueOrNull(\"ForceRandomStartLocations\");\n\n            // ForceNoTeams\n            ForceNoTeams = section.GetBooleanValueOrNull(\"ForceNoTeams\");\n\n            // CoopDifficultyLevel\n            CoopDifficultyLevel = section.GetIntValueOrNull(\"CoopDifficultyLevel\");\n        }\n\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GameModeMapCollection.cs",
    "content": "﻿#nullable enable\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\n\nusing ClientCore;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class GameModeMapCollection : IReadOnlyGameModeMapCollection\n    {\n        private readonly List<GameModeMap> items;\n        private readonly Dictionary<string, Map> mapHashIndex;\n\n        // Note: whenever `items` is modified, we must invalidate the cached GameModes list by setting `_gameModes` to null.\n        private List<GameMode>? _gameModes = null;\n        public IReadOnlyList<GameMode> GameModes => _gameModes ??= items.Select(gmm => gmm.GameMode).Distinct().ToList();\n\n        public GameModeMapCollection(IEnumerable<GameMode> gameModes)\n        {\n            // Build the list of GameModeMaps\n            items = gameModes.SelectMany(gm => gm.Maps.Select(map =>\n                new GameModeMap(gm, map, UserINISettings.Instance.IsFavoriteMap(map.SHA1, map.UntranslatedName, gm.Name))))\n                .Distinct()\n                .ToList();\n\n            // Build the hash index for fast lookups\n            mapHashIndex = new Dictionary<string, Map>(StringComparer.OrdinalIgnoreCase);\n            foreach (var gameModeMap in items)\n            {\n                var map = gameModeMap.Map;\n                if (!string.IsNullOrEmpty(map.SHA1) && !mapHashIndex.ContainsKey(map.SHA1))\n                    mapHashIndex[map.SHA1] = map;\n            }\n        }\n\n        /// <summary>\n        /// Finds a map by its SHA1 hash with optimized performance.\n        /// </summary>\n        /// <param name=\"mapHash\">The SHA1 hash of the map.</param>\n        /// <returns>The map if found, null otherwise.</returns>\n        public Map? FindMapByHash(string mapHash)\n        {\n            if (string.IsNullOrEmpty(mapHash))\n                return null;\n\n            mapHashIndex.TryGetValue(mapHash, out Map? map);\n            return map;\n        }\n\n        /// <summary>\n        /// Adds the specified game mode map to the collection.\n        /// </summary>\n        /// <param name=\"gameModeMap\">The game mode map to add to the collection.</param>\n        public void Add(GameModeMap gameModeMap)\n        {\n            items.Add(gameModeMap);\n            _gameModes = null;\n\n            // Update the hash index\n            Map? map = gameModeMap?.Map;\n            if (map != null)\n            {\n                string sha1 = map.SHA1;\n\n                if (!string.IsNullOrEmpty(sha1) && !mapHashIndex.ContainsKey(sha1))\n                    mapHashIndex[sha1] = map;\n            }\n        }\n\n        /// <summary>\n        /// Adds a range of GameModeMaps to the collection and updates the hash index.\n        /// </summary>\n        public void AddRange(IEnumerable<GameModeMap> gameModeMapCollection)\n        {\n            foreach (var gameModeMap in gameModeMapCollection)\n                Add(gameModeMap);\n        }\n\n        /// <summary>\n        /// Removes a GameModeMap from the collection and updates the hash index if needed.\n        /// </summary>\n        public bool Remove(GameModeMap gameModeMap)\n        {\n            bool removed = items.Remove(gameModeMap);\n\n            if (removed)\n            {\n                _gameModes = null;\n\n                var map = gameModeMap.Map;\n                // Only remove from index if no other GameModeMap references this map\n                if (!string.IsNullOrEmpty(map.SHA1) &&\n                    !items.Any(gmm => string.Equals(gmm.Map.SHA1, map.SHA1, StringComparison.OrdinalIgnoreCase)))\n                    mapHashIndex.Remove(map.SHA1);\n            }\n\n            return removed;\n        }\n\n        public GameModeMap this[int index] => items[index];\n        public int Count => items.Count;\n\n        public IEnumerator<GameModeMap> GetEnumerator() => items.GetEnumerator();\n        IEnumerator IEnumerable.GetEnumerator() => items.GetEnumerator();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GameOptionPresets.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A single game option preset.\n    /// </summary>\n    public class GameOptionPreset\n    {\n        public GameOptionPreset(string profileName)\n        {\n            ProfileName = profileName;\n\n            if (ProfileName.Contains('[') || ProfileName.Contains(']'))\n                throw new ArgumentException(\"Game option preset name cannot contain the [] characters.\");\n        }\n\n        /// <summary>\n        /// Checks if a specific name is valid for the name of a game option preset.\n        /// Returns null if the name is valid, an error message otherwise.\n        /// </summary>\n        public static string IsNameValid(string name)\n        {\n            if (name.Contains('[') || name.Contains(']'))\n                return \"Game option preset name cannot contain the [] characters.\";\n\n            return null;\n        }\n\n        public string ProfileName { get; }\n\n        private Dictionary<string, bool> checkBoxValues = new Dictionary<string, bool>();\n        private Dictionary<string, int> dropDownValues = new Dictionary<string, int>();\n\n        private void AddValues<T>(IniSection section, string keyName, Dictionary<string, T> dictionary, Converter<string, T> converter)\n        {\n            string[] valueStrings = section.GetStringValue(keyName,\n                string.Empty).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);\n\n            foreach (string value in valueStrings)\n            {\n                string[] splitValue = value.Split(':');\n                if (splitValue.Length != 2)\n                {\n                    Logger.Log($\"Failed to parse game option preset value ({ProfileName}, {keyName})\");\n                    continue;\n                }\n\n                dictionary.Add(splitValue[0], converter(splitValue[1]));\n            }\n        }\n\n        public void AddCheckBoxValue(string checkBoxName, bool value)\n        {\n            checkBoxValues.Add(checkBoxName, value);\n        }\n\n        public void AddDropDownValue(string dropDownValue, int value)\n        {\n            dropDownValues.Add(dropDownValue, value);\n        }\n\n        public Dictionary<string, bool> GetCheckBoxValues() => new Dictionary<string, bool>(checkBoxValues);\n        public Dictionary<string, int> GetDropDownValues() => new Dictionary<string, int>(dropDownValues);\n\n        public void Read(IniSection section)\n        {\n            // Syntax example:\n            // CheckBoxValues=chkCrates:1,chkShortGame:1,chkFastResourceGrowth:0,.... (0 = unchecked, 1 = checked)\n            // DropDownValues=ddTechLevel:7,ddStartingCredits:5,... (the number is the selected option index)\n\n            AddValues(section, \"CheckBoxValues\", checkBoxValues, s => s == \"1\");\n            AddValues(section, \"DropDownValues\", dropDownValues, s => Conversions.IntFromString(s, 0));\n        }\n\n        public void Write(IniSection section)\n        {\n            section.SetStringValue(\"CheckBoxValues\", string.Join(\",\",\n                checkBoxValues.Select(s => $\"{s.Key}:{(s.Value ? \"1\" : \"0\")}\")));\n            section.SetStringValue(\"DropDownValues\", string.Join(\",\",\n                dropDownValues.Select(s => $\"{s.Key}:{s.Value.ToString()}\")));\n        }\n    }\n\n    /// <summary>\n    /// Handles game option presets.\n    /// </summary>\n    public class GameOptionPresets\n    {\n        private const string IniFileName = \"GameOptionsPresets.ini\";\n        private const string PresetDefinitionsSectionName = \"Presets\";\n\n        private GameOptionPresets() { }\n\n        private static GameOptionPresets _instance;\n        public static GameOptionPresets Instance\n        {\n            get\n            {\n                if (_instance == null)\n                    _instance = new GameOptionPresets();\n\n                return _instance;\n            }\n        }\n\n        private IniFile gameOptionPresetsIni;\n        private Dictionary<string, GameOptionPreset> presets;\n\n        public GameOptionPreset GetPreset(string name)\n        {\n            LoadIniIfNotInitialized();\n\n            if (presets.TryGetValue(name, out GameOptionPreset value))\n                return value;\n\n            return null;\n        }\n\n        public List<string> GetPresetNames()\n        {\n            LoadIniIfNotInitialized();\n\n            return presets.Keys\n                .Where(key => !string.IsNullOrWhiteSpace(key))\n                .ToList();\n        }\n\n        public void AddPreset(GameOptionPreset preset)\n        {\n            LoadIniIfNotInitialized();\n\n            presets[preset.ProfileName] = preset;\n            WriteIni();\n        }\n\n        public void DeletePreset(string name)\n        {\n            LoadIniIfNotInitialized();\n\n            if (!presets.ContainsKey(name))\n                return;\n\n            presets.Remove(name);\n            WriteIni();\n        }\n\n        private void LoadIniIfNotInitialized()\n        {\n            if (gameOptionPresetsIni == null)\n                LoadIni();\n        }\n\n        private void LoadIni()\n        {\n            gameOptionPresetsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, IniFileName));\n            presets = new Dictionary<string, GameOptionPreset>();\n\n            IniSection presetsDefinitions = gameOptionPresetsIni.GetSection(PresetDefinitionsSectionName);\n            if (presetsDefinitions == null)\n                return;\n\n            foreach (var kvp in presetsDefinitions.Keys)\n            {\n                if (!presets.ContainsKey(kvp.Value))\n                {\n                    IniSection presetSection = gameOptionPresetsIni.GetSection(kvp.Value);\n                    if (presetSection == null)\n                        continue;\n\n                    var preset = new GameOptionPreset(kvp.Value);\n                    preset.Read(presetSection);\n                    presets[kvp.Value] = preset;\n                }\n            }\n        }\n\n        private void WriteIni()\n        {\n            gameOptionPresetsIni = new IniFile();\n            int i = 0;\n            var definitionsSection = new IniSection(PresetDefinitionsSectionName);\n            gameOptionPresetsIni.AddSection(definitionsSection);\n            foreach (var kvp in presets)\n            {\n                definitionsSection.SetStringValue(i.ToString(), kvp.Value.ProfileName);\n                var presetSection = new IniSection(kvp.Value.ProfileName);\n                kvp.Value.Write(presetSection);\n                gameOptionPresetsIni.AddSection(presetSection);\n                i++;\n            }\n\n            gameOptionPresetsIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, IniFileName));\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/GenericHostedGame.cs",
    "content": "﻿using DTAClient.Domain.Multiplayer.CnCNet;\nusing System;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A base class for hosted games.\n    /// CnCNet and LAN games derive from this.\n    /// </summary>\n    public abstract class GenericHostedGame: IEquatable<GenericHostedGame>\n    {\n        public virtual string RoomName { get; set; }\n        public bool Incompatible { get; set; }\n        public bool Locked { get; set; }\n        public bool IsLoadedGame { get; set; }\n        public bool Passworded { get; set; }\n        public CnCNetGame Game { get; set; }\n        public string GameMode { get; set; }\n        public string Map { get; set; }\n        public string MapHash { get; set; }\n        public string GameVersion { get; set; }\n        public string HostName { get; set; }\n        public string[] Players { get; set; }\n\n        public int MaxPlayers { get; set; } = 8;\n\n        public abstract int Ping { get; }\n\n        public DateTime LastRefreshTime { get; set; }\n\n        public int SkillLevel { get; set; }\n\n        public virtual bool Equals(GenericHostedGame other)\n            => string.Equals(RoomName, other?.RoomName, StringComparison.InvariantCultureIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/ICacheManager.cs",
    "content": "﻿#nullable enable\nusing System;\n\n\nnamespace DTAClient.Domain.Multiplayer;\n\npublic interface ICacheManager<TInput, TOutput> : IDisposable\n{\n    /// <summary>\n    /// Gets the number of elements contained in the collection.\n    /// </summary>\n    public int Count { get; }\n\n    /// <summary>\n    /// 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.\n    /// </summary>\n    public void Clear();\n\n    /// <summary>\n    /// Requests an output to be computed for the specified input.\n    /// </summary>\n    /// <param name=\"input\">The input to get the output.</param>\n    /// <param name=\"output\">The cached output if found or computed.</param>\n    /// <param name=\"syncComputeOnCacheMiss\">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 <see cref=\"addToQueue\"/> holds.</param>\n    /// <param name=\"addToQueue\">This parameter is ignored if <see cref=\"syncComputeOnCacheMiss\"/> 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.</param>\n    /// <returns>True if the output was found in cache or computed synchronously; false if the output is not available yet.</returns>\n    public bool Request(TInput input, out TOutput? output, bool syncComputeOnCacheMiss = false, bool addToQueue = true);\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/IGameModeMap.cs",
    "content": "﻿#nullable enable\nusing System.Collections.Generic;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public interface IGameModeMap\n    {\n        public List<int> AllowedStartingLocations { get; }\n        public int CoopDifficultyLevel { get; }\n        public CoopMapInfo? CoopInfo { get; }\n        public bool EnforceMaxPlayers { get; }\n        public bool ForceNoTeams { get; }\n        public bool ForceRandomStartLocations { get; }\n        public bool HumanPlayersOnly { get; }\n        public bool IsCoop { get; }\n        public int MaxPlayers { get; }\n        public int MinPlayers { get; }\n        public bool MultiplayerOnly { get; }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/IMapPreviewCacheManager.cs",
    "content": "﻿#nullable enable\nusing System;\n\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer;\n\npublic interface IMapPreviewCacheManager : ICacheManager<Map, Image> { }"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/IReadOnlyGameModeMapCollection.cs",
    "content": "﻿#nullable enable\nusing System.Collections.Generic;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public interface IReadOnlyGameModeMapCollection : IReadOnlyList<GameModeMap>\n    {\n        public IReadOnlyList<GameMode> GameModes { get; }\n        public Map? FindMapByHash(string mapHash);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class ClientIntCommandHandler : LANClientCommandHandler\n    {\n        public ClientIntCommandHandler(string commandName, Action<int> action) : base(commandName)\n        {\n            this.action = action;\n        }\n\n        private Action<int> action;\n\n        public override bool Handle(string message)\n        {\n            if (!message.StartsWith(CommandName))\n                return false;\n\n            if (message.Length < CommandName.Length + 2)\n                return false;\n\n            int value;\n            bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value);\n\n            if (!success)\n                return false;\n\n            action(value);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/ClientNoParamCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    /// <summary>\n    /// A command handler that has no parameters.\n    /// </summary>\n    class ClientNoParamCommandHandler : LANClientCommandHandler\n    {\n        public ClientNoParamCommandHandler(string commandName, Action commandHandler) : base(commandName)\n        {\n            this.commandHandler = commandHandler;\n        }\n\n        Action commandHandler;\n\n        public override bool Handle(string message)\n        {\n            if (message != CommandName)\n                return false;\n\n            commandHandler();\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class ClientStringCommandHandler : LANClientCommandHandler\n    {\n        public ClientStringCommandHandler(string commandName, Action<string> action) : base(commandName)\n        {\n            this.action = action;\n        }\n\n        Action<string> action;\n\n        public override bool Handle(string message)\n        {\n            if (!message.StartsWith(CommandName))\n                return false;\n\n            action(message.Substring(CommandName.Length + 1));\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs",
    "content": "﻿using System;\nusing System.Net;\n\nusing ClientCore;\n\nusing DTAClient.Domain.Multiplayer;\nusing DTAClient.Domain.Multiplayer.CnCNet;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain.LAN\n{\n    class HostedLANGame : GenericHostedGame\n    {\n        public IPEndPoint EndPoint { get; set; }\n\n        public override string RoomName\n        {\n            get => HostName + \"'s Game\" + (EndPoint != null ? \" [\" + EndPoint.Address.ToString() + \"]\" : \"\");\n            set\n            {\n                // RoomName is generated from HostName and EndPoint. Setting it has no effect.\n            }\n        }\n\n        public string LoadedGameID { get; set; }\n\n        public TimeSpan TimeWithoutRefresh { get; set; }\n\n        public override int Ping\n        {\n            get\n            {\n                return -1;\n            }\n        }\n\n        public bool SetDataFromStringArray(GameCollection gc, string[] parameters)\n        {\n            if (parameters.Length != 10)\n            {\n                Logger.Log(\"Ignoring LAN GAME message because of an incorrect number of parameters.\");\n                return false;\n            }\n\n            if (parameters[0] != ProgramConstants.LAN_PROTOCOL_REVISION)\n                return false;\n\n            GameVersion = parameters[1];\n            Incompatible = GameVersion != ProgramConstants.GAME_VERSION;\n            Game = gc.GameList.Find(g => g.InternalName.ToUpperInvariant() == parameters[2]);\n            if (Game == null)\n                return false;\n            Map = parameters[3];\n            GameMode = parameters[4];\n            LoadedGameID = parameters[5];\n            string[] players = parameters[6].Split(',');\n            Players = players;\n            if (players.Length == 0)\n                return false;\n            HostName = players[0];\n            Locked = Conversions.IntFromString(parameters[7], 1) > 0;\n            IsLoadedGame = Conversions.IntFromString(parameters[8], 0) > 0;\n            LastRefreshTime = DateTime.Now;\n            TimeWithoutRefresh = TimeSpan.Zero;\n\n            // RoomName is now generated from HostName and EndPoint. Setting it has no effect.\n            // RoomName = HostName + \"'s Game\";\n\n            MapHash = parameters[9];\n\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/LANClientCommandHandler.cs",
    "content": "﻿namespace DTAClient.Domain.Multiplayer.LAN\n{\n    public abstract class LANClientCommandHandler\n    {\n        public LANClientCommandHandler(string commandName)\n        {\n            CommandName = commandName;\n        }\n\n        public string CommandName { get; private set; }\n\n        public abstract bool Handle(string message);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/LANColor.cs",
    "content": "﻿using Microsoft.Xna.Framework;\n\nnamespace DTAClient.Domain.LAN\n{\n    public class LANColor\n    {\n        public LANColor(string name, Color xnaColor)\n        {\n            Name = name;\n            XNAColor = xnaColor;\n        }\n\n        public string Name { get; private set; }\n        public Color XNAColor { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/LANLobbyUser.cs",
    "content": "﻿using System;\nusing System.Net;\n\nusing Microsoft.Xna.Framework.Graphics;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class LANLobbyUser\n    {\n        public LANLobbyUser(string name, Texture2D gameTexture, IPEndPoint endPoint)\n        {\n            Name = name;\n            GameTexture = gameTexture;\n            EndPoint = endPoint;\n        }\n\n        public string Name { get; private set; }\n        public Texture2D GameTexture { get; private set; }\n        public IPEndPoint EndPoint { get; private set; }\n\n        private readonly object timeWithoutRefreshLock = new();\n        public TimeSpan TimeWithoutRefresh { get; private set; }\n\n        public void ClearTimeWithoutRefresh()\n        {\n            lock (timeWithoutRefreshLock)\n            {\n                TimeWithoutRefresh = TimeSpan.Zero;\n            }\n        }\n\n        public void AddToTimeWithoutRefresh(TimeSpan timeToAdd)\n        {\n            lock (timeWithoutRefreshLock)\n            {\n                TimeWithoutRefresh += timeToAdd;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs",
    "content": "﻿using ClientCore;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class LANPlayerInfo : PlayerInfo\n    {\n        public LANPlayerInfo(Encoding encoding)\n        {\n            this.encoding = encoding;\n            Port = PORT;\n        }\n\n        public event EventHandler<NetworkMessageEventArgs> MessageReceived;\n        public event EventHandler ConnectionLost;\n        public event EventHandler PlayerPinged;\n\n        private const int PORT = 1234;\n        private const int LOBBY_PORT = 1233;\n        private const double SEND_PING_TIMEOUT = 10.0;\n        private const double DROP_TIMEOUT = 20.0;\n        private const int LAN_PING_TIMEOUT = 1000;\n\n        public TimeSpan TimeSinceLastReceivedMessage { get; set; }\n        public TimeSpan TimeSinceLastSentMessage { get; set; }\n\n        public TcpClient TcpClient { get; private set; }\n\n        NetworkStream networkStream;\n\n        Encoding encoding;\n\n        string overMessage = string.Empty;\n\n        public void SetClient(TcpClient client)\n        {\n            if (TcpClient != null)\n                throw new InvalidOperationException(\"TcpClient has already been set for this LANPlayerInfo!\");\n\n            TcpClient = client;\n            TcpClient.SendTimeout = 1000;\n            networkStream = client.GetStream();\n        }\n\n        /// <summary>\n        /// Updates logic timers for the player.\n        /// </summary>\n        /// <param name=\"gameTime\">Provides a snapshot of timing values.</param>\n        /// <returns>True if the player is still considered connected, otherwise false.</returns>\n        public bool Update(GameTime gameTime)\n        {\n            TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime;\n            TimeSinceLastSentMessage += gameTime.ElapsedGameTime;\n\n            if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)\n                || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT))\n                SendMessage(\"PING\");\n\n            if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT))\n                return false;\n\n            return true;\n        }\n\n        public override string IPAddress\n        {\n            get\n            {\n                if (TcpClient != null)\n                    return ((IPEndPoint)TcpClient.Client.RemoteEndPoint).Address.ToString();\n\n                return base.IPAddress;\n            }\n\n            set\n            {\n                base.IPAddress = value;\n                //throw new InvalidOperationException(\"Cannot set LANPlayerInfo's IPAddress!\");\n            }\n        }\n\n        /// <summary>\n        /// Sends a message to the player over the network.\n        /// </summary>\n        /// <param name=\"message\">The message to send.</param>\n        public void SendMessage(string message)\n        {\n            byte[] buffer;\n\n            buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n            try\n            {\n                networkStream.Write(buffer, 0, buffer.Length);\n                networkStream.Flush();\n            }\n            catch\n            {\n                Logger.Log(\"Sending message to \" + ToString() + \" failed!\");\n            }\n\n            TimeSinceLastSentMessage = TimeSpan.Zero;\n        }\n\n        public override string ToString()\n        {\n            return Name + \" (\" + IPAddress + \")\";\n        }\n\n        /// <summary>\n        /// Starts receiving messages from the player asynchronously.\n        /// </summary>\n        public void StartReceiveLoop()\n        {\n            Thread thread = new Thread(ReceiveMessages);\n            thread.Start();\n        }\n\n        /// <summary>\n        /// Receives messages sent by the client,\n        /// and hands them over to another class via an event.\n        /// </summary>\n        private void ReceiveMessages()\n        {\n            byte[] message = new byte[1024];\n\n            string msg = String.Empty;\n\n            int bytesRead = 0;\n\n            NetworkStream ns = TcpClient.GetStream();\n\n            while (true)\n            {\n                bytesRead = 0;\n\n                try\n                {\n                    //blocks until a client sends a message\n                    bytesRead = ns.Read(message, 0, message.Length);\n                }\n                catch (Exception ex)\n                {\n                    //a socket error has occured\n                    Logger.Log(\"Socket error with client \" + Name + \"; removing. Message: \" + ex.ToString());\n                    ConnectionLost?.Invoke(this, EventArgs.Empty);\n                    break;\n                }\n\n                if (bytesRead > 0)\n                {\n                    msg = encoding.GetString(message, 0, bytesRead);\n\n                    msg = overMessage + msg;\n                    List<string> commands = new List<string>();\n\n                    while (true)\n                    {\n                        int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR);\n\n                        if (index == -1)\n                        {\n                            overMessage = msg;\n                            break;\n                        }\n                        else\n                        {\n                            commands.Add(msg.Substring(0, index));\n                            msg = msg.Substring(index + 1);\n                        }\n                    }\n\n                    foreach (string cmd in commands)\n                    {\n                        MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd));\n                    }\n\n                    continue;\n                }\n\n                ConnectionLost?.Invoke(this, EventArgs.Empty);\n                break;\n            }\n        }\n\n        public void UpdatePing(WindowManager wm)\n        {\n            using (Ping p = new Ping())\n            {\n                try\n                {\n                    PingReply reply = p.Send(System.Net.IPAddress.Parse(IPAddress), LAN_PING_TIMEOUT);\n                    if (reply.Status == IPStatus.Success)\n                        Ping = Convert.ToInt32(reply.RoundtripTime);\n\n                    wm.AddCallback(PlayerPinged, this, EventArgs.Empty);\n                }\n                catch (PingException ex)\n                {\n                    Logger.Log($\"Caught an exception when pinging {Name} LAN player: {ex.ToString()}\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs",
    "content": "﻿namespace DTAClient.Domain.Multiplayer.LAN\n{\n    public abstract class LANServerCommandHandler\n    {\n        public LANServerCommandHandler(string commandName)\n        {\n            CommandName = commandName;\n        }\n\n        public string CommandName { get; private set; }\n\n        public abstract bool Handle(LANPlayerInfo pInfo, string message);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/NetworkMessageEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class NetworkMessageEventArgs : EventArgs\n    {\n        public NetworkMessageEventArgs(string message)\n        {\n            Message = message;\n        }\n\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class ServerNoParamCommandHandler : LANServerCommandHandler\n    {\n        public ServerNoParamCommandHandler(string commandName,\n            Action<LANPlayerInfo> handler) : base(commandName)\n        {\n            this.handler = handler;\n        }\n\n        Action<LANPlayerInfo> handler;\n\n        public override bool Handle(LANPlayerInfo pInfo, string message)\n        {\n            if (message == CommandName)\n            {\n                handler(pInfo);\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer.LAN\n{\n    public class ServerStringCommandHandler : LANServerCommandHandler\n    {\n        public ServerStringCommandHandler(string commandName,\n            Action<LANPlayerInfo, string> handler)\n            : base(commandName)\n        {\n            this.handler = handler;\n        }\n\n        Action<LANPlayerInfo, string> handler;\n\n        public override bool Handle(LANPlayerInfo pInfo, string message)\n        {\n            if (!message.StartsWith(CommandName) ||\n                message.Length <= CommandName.Length + 1)\n                return false;\n\n            handler(pInfo, message);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/Map.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing System.Text.Json.Serialization;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nusing Rampastring.Tools;\n\nusing SixLabors.ImageSharp;\n\nusing Point = Microsoft.Xna.Framework.Point;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public struct ExtraMapPreviewTexture\n    {\n        public string TextureName;\n        public Point Point;\n        public int Level;\n        public bool Toggleable;\n\n        public ExtraMapPreviewTexture(string textureName, Point point, int level, bool toggleable)\n        {\n            TextureName = textureName;\n            Point = point;\n            Level = level;\n            Toggleable = toggleable;\n        }\n    }\n\n    /// <summary>\n    /// A multiplayer map.\n    /// </summary>\n    public class Map : GameModeMapBase\n    {\n        [JsonConstructor]\n        public Map(string baseFilePath)\n            : this(baseFilePath, true)\n        {\n        }\n\n        public Map(string baseFilePath, bool isCustomMap)\n        {\n            if (string.IsNullOrWhiteSpace(baseFilePath))\n                throw new ArgumentNullException(nameof(baseFilePath));\n\n            Debug.Assert(!baseFilePath.EndsWith($\".{ClientConfiguration.Instance.MapFileExtension}\", StringComparison.InvariantCultureIgnoreCase), $\"Unexpected map path {baseFilePath}. It should not end with the map extension.\");\n\n            BaseFilePath = baseFilePath;\n            customMapFilePath = isCustomMap\n                ? SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($\"{baseFilePath}.{ClientConfiguration.Instance.MapFileExtension}\"))\n                : null;\n            Official = string.IsNullOrWhiteSpace(customMapFilePath);\n        }\n\n        /// <summary>\n        /// The name of the map.\n        /// </summary>\n        [JsonIgnore]\n        public string Name => !Official || string.IsNullOrEmpty(UntranslatedName) || string.IsNullOrEmpty(BaseFilePath)\n            ? UntranslatedName\n            : UntranslatedName.L10N($\"INI:Maps:{BaseFilePath}:Description\");\n\n        /// <summary>\n        /// The original untranslated name of the map.\n        /// </summary>\n        [JsonInclude]\n        public string UntranslatedName\n        {\n            get => field;\n            private set\n            {\n                field = value;\n                // Force triggering localization of the name now\n                _ = Name;\n            }\n        }\n\n        /// <summary>\n        /// If set, this map won't be automatically transferred over CnCNet when\n        /// a player doesn't have it.\n        /// </summary>\n        [JsonIgnore]\n        public bool Official { get; private set; }\n\n        /// <summary>\n        /// The briefing of the map.\n        /// </summary>\n        [JsonInclude]\n        public string Briefing { get; private set; }\n\n        /// <summary>\n        /// The author of the map.\n        /// </summary>\n        [JsonInclude]\n        public string Author { get; private set; }\n\n        /// <summary>\n        /// The calculated SHA1 hash of the map.\n        /// </summary>\n        [JsonInclude]\n        public string SHA1 { get; private set; } = null;\n\n        /// <summary>\n        /// The path to the map file.\n        /// </summary>\n        [JsonInclude]\n        public string BaseFilePath { get; private set; }\n\n        /// <summary>\n        /// Returns the complete path to the map file.\n        /// Includes the game directory in the path.\n        /// </summary>\n        [JsonIgnore]\n        public string CompleteFilePath => SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($\"{BaseFilePath}.{ClientConfiguration.Instance.MapFileExtension}\"));\n\n        /// <summary>\n        /// The file name of the preview image.\n        /// </summary>\n        [JsonInclude]\n        public string PreviewPath { get; private set; }\n\n        /// <summary>\n        /// The game modes that the map is listed for.\n        /// </summary>\n        [JsonInclude]\n        public string[] GameModes;\n\n        /// <summary>\n        /// The forced UnitCount for the map. -1 means none.\n        /// </summary>\n        [JsonInclude]\n        public int UnitCount = -1;\n\n        /// <summary>\n        /// The forced starting credits for the map. -1 means none.\n        /// </summary>\n        [JsonInclude]\n        public int Credits = -1;\n\n        [JsonInclude]\n        public int NeutralHouseColor = -1;\n\n        [JsonInclude]\n        public int SpecialHouseColor = -1;\n\n        [JsonInclude]\n        public int Bases = -1;\n\n        [JsonInclude]\n        public string[] localSize;\n\n        [JsonInclude]\n        public string[] actualSize;\n\n        [JsonInclude]\n        public int x;\n\n        [JsonInclude]\n        public int y;\n\n        [JsonInclude]\n        public int width;\n\n        [JsonInclude]\n        public int height;\n\n        /// <summary>\n        /// The full path of custom map INI file. It gets re-initialized in JsonConstructor, so it won't be serialized / deserialized directly.\n        /// </summary>\n        [JsonIgnore]\n        private readonly string customMapFilePath;\n\n        [JsonInclude]\n        public List<string> waypoints = new List<string>();\n\n        /// <summary>\n        /// The pixel coordinates of the map's player starting locations.\n        /// </summary>\n        [JsonInclude]\n        public List<Point> startingLocations;\n\n        [JsonInclude]\n        public List<TeamStartMappingPreset> TeamStartMappingPresets = new List<TeamStartMappingPreset>();\n\n        [JsonIgnore]\n        public List<TeamStartMapping> TeamStartMappings => TeamStartMappingPresets?.FirstOrDefault()?.TeamStartMappings;\n\n        public void CalculateSHA()\n        {\n            SHA1 = Utilities.CalculateSHA1ForFile(CompleteFilePath);\n        }\n\n        [JsonInclude]\n        public List<KeyValuePair<string, bool>> ForcedCheckBoxValues = new List<KeyValuePair<string, bool>>(0);\n\n        [JsonInclude]\n        public List<KeyValuePair<string, int>> ForcedDropDownValues = new List<KeyValuePair<string, int>>(0);\n\n        [JsonIgnore]\n        private List<ExtraMapPreviewTexture> extraTextures = new List<ExtraMapPreviewTexture>(0);\n\n        public List<ExtraMapPreviewTexture> GetExtraMapPreviewTextures() => extraTextures;\n\n        [JsonIgnore]\n        private List<KeyValuePair<string, string>> ForcedSpawnIniOptions = new List<KeyValuePair<string, string>>(0);\n\n        /// <summary>\n        /// The name of an extra INI file in INI\\Map Code\\ that should be\n        /// embedded into this map's INI code when a game is started.\n        /// </summary>\n        [JsonInclude]\n        public string ExtraININame { get; private set; }\n\n        /// <summary>\n        /// This is used to load a map from the MPMaps.ini (default name) file.\n        /// </summary>\n        /// <param name=\"iniFile\">The configuration file for the multiplayer maps.</param>\n        /// <returns>True if loading the map succeeded, otherwise false.</returns>\n        public bool InitializeFromMpMapsINI(IniFile iniFile)\n        {\n            try\n            {\n                string baseSectionName = iniFile.GetStringValue(BaseFilePath, \"BaseSection\", string.Empty);\n\n                if (!string.IsNullOrEmpty(baseSectionName))\n                    iniFile.CombineSections(baseSectionName, BaseFilePath);\n\n                var section = iniFile.GetSection(BaseFilePath);\n\n                UntranslatedName = section.GetStringValue(\"Description\", \"Unnamed map\");\n\n                Author = section.GetStringValue(\"Author\", \"Unknown author\");\n                GameModes = section.GetStringValue(\"GameModes\", \"Default\").Split(',');\n\n                // Initialize PreviewPath\n                {\n                    FileInfo mapFile = SafePath.GetFile(BaseFilePath);\n                    string previewPath = SafePath.CombineFilePath(SafePath.GetDirectory(mapFile.FullName).Parent.FullName[ProgramConstants.GamePath.Length..], FormattableString.Invariant($\"{section.GetStringValue(\"PreviewImage\", mapFile.Name)}.png\"));\n                    if (!SafePath.GetFile(ProgramConstants.GamePath, previewPath).Exists)\n                        previewPath = null;\n\n                    PreviewPath = previewPath;\n                }\n\n                Briefing = section.GetStringValue(\"Briefing\", string.Empty)\n                    .FromIniString()\n                    .L10N($\"INI:Maps:{BaseFilePath}:Briefing\");\n\n                CalculateSHA();\n\n                InitializeBaseSettingsFromIniSection(section, isCustomMap: false);\n\n                Credits = section.GetIntValue(\"Credits\", -1);\n                UnitCount = section.GetIntValue(\"UnitCount\", -1);\n                NeutralHouseColor = section.GetIntValue(\"NeutralColor\", -1);\n                SpecialHouseColor = section.GetIntValue(\"SpecialColor\", -1);\n\n                string bases = section.GetStringValue(\"Bases\", string.Empty);\n                if (!string.IsNullOrEmpty(bases))\n                {\n                    Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false));\n                }\n\n                int i = 0;\n                while (true)\n                {\n                    // Format example:\n                    // ExtraTexture0=oilderrick.png,200,150,1,false\n                    // Third value is optional map cell level, defaults to 0 if unspecified.\n                    // Fourth value is optional boolean value that determines if the texture can be toggled on / off.\n                    string value = section.GetStringValue(\"ExtraTexture\" + i, null);\n\n                    if (string.IsNullOrWhiteSpace(value))\n                        break;\n\n                    string[] parts = value.Split(',');\n\n                    if (parts.Length is < 3 or > 5)\n                    {\n                        Logger.Log($\"Invalid format for ExtraTexture{i} in map \" + BaseFilePath);\n                        continue;\n                    }\n\n                    bool success = int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int x);\n                    success &= int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int y);\n\n                    int level = 0;\n                    bool toggleable = false;\n\n                    if (parts.Length > 3)\n                        int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out level);\n\n                    if (parts.Length > 4)\n                        toggleable = Conversions.BooleanFromString(parts[4], false);\n\n                    extraTextures.Add(new ExtraMapPreviewTexture(parts[0], new Point(x, y), level, toggleable));\n\n                    i++;\n                }\n\n                if (MainClientConstants.USE_ISOMETRIC_CELLS)\n                {\n                    localSize = section.GetStringValue(\"LocalSize\", \"0,0,0,0\").Split(',');\n                    actualSize = section.GetStringValue(\"Size\", \"0,0,0,0\").Split(',');\n                }\n                else\n                {\n                    x = section.GetIntValue(\"X\", 0);\n                    y = section.GetIntValue(\"Y\", 0);\n                    width = section.GetIntValue(\"Width\", 0);\n                    height = section.GetIntValue(\"Height\", 0);\n                }\n\n                for (i = 0; i < MAX_PLAYERS; i++)\n                {\n                    string waypoint = section.GetStringValue(\"Waypoint\" + i, string.Empty);\n\n                    if (string.IsNullOrEmpty(waypoint))\n                        break;\n\n                    Debug.Assert(int.TryParse(waypoint.Split(',')[0], out _), $\"waypoint should be a number, got {waypoint}\");\n                    waypoints.Add(waypoint);\n                }\n\n                GetTeamStartMappingPresets(section);\n\n                // Parse forced options\n\n                string forcedOptionsSections = iniFile.GetStringValue(BaseFilePath, \"ForcedOptions\", string.Empty);\n\n                if (!string.IsNullOrEmpty(forcedOptionsSections))\n                {\n                    string[] sections = forcedOptionsSections.Split(',');\n                    foreach (string foSection in sections)\n                        ParseForcedOptions(iniFile, foSection);\n                }\n\n                string forcedSpawnIniOptionsSections = iniFile.GetStringValue(BaseFilePath, \"ForcedSpawnIniOptions\", string.Empty);\n\n                if (!string.IsNullOrEmpty(forcedSpawnIniOptionsSections))\n                {\n                    string[] sections = forcedSpawnIniOptionsSections.Split(',');\n                    foreach (string fsioSection in sections)\n                        ParseSpawnIniOptions(iniFile, fsioSection);\n                }\n\n                ExtraININame = section.GetStringValueOrNull(\"ExtraININame\");\n\n                return true;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Setting info for \" + BaseFilePath + \" failed! Reason: \" + ex.ToString());\n                PreStartup.LogException(ex);\n                return false;\n            }\n        }\n\n        private void GetTeamStartMappingPresets(IniSection section)\n        {\n            TeamStartMappingPresets = new List<TeamStartMappingPreset>();\n            for (int i = 0; ; i++)\n            {\n                try\n                {\n                    var teamStartMappingPreset = section.GetStringValue($\"TeamStartMapping{i}\", string.Empty);\n                    if (string.IsNullOrEmpty(teamStartMappingPreset))\n                        return; // mapping not found\n\n                    var teamStartMappingPresetName = section.GetStringValue($\"TeamStartMapping{i}Name\", string.Empty);\n                    if (string.IsNullOrEmpty(teamStartMappingPresetName))\n                        continue; // mapping found, but no name specified\n\n                    TeamStartMappingPresets.Add(new TeamStartMappingPreset()\n                    {\n                        Name = teamStartMappingPresetName,\n                        TeamStartMappings = TeamStartMapping.FromListString(teamStartMappingPreset)\n                    });\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"Unable to parse team start mappings. Map: \\\"{Name}\\\", Error: {ex.Message}\");\n                    TeamStartMappingPresets = new List<TeamStartMappingPreset>();\n                }\n            }\n        }\n\n        public List<Point> GetStartingLocationPreviewCoords(Point previewSize)\n        {\n            if (startingLocations == null)\n            {\n                startingLocations = new List<Point>();\n\n                foreach (string waypoint in waypoints)\n                {\n                    if (MainClientConstants.USE_ISOMETRIC_CELLS)\n                        startingLocations.Add(GetIsometricWaypointCoords(waypoint, actualSize, localSize, previewSize));\n                    else\n                        startingLocations.Add(GetTDRAWaypointCoords(waypoint, x, y, width, height, previewSize));\n                }\n            }\n\n            return startingLocations;\n        }\n\n        public Point MapPointToMapPreviewPoint(Point mapPoint, Point previewSize, int level)\n        {\n            if (MainClientConstants.USE_ISOMETRIC_CELLS)\n                return GetIsoTilePixelCoord(mapPoint.X, mapPoint.Y, actualSize, localSize, previewSize, level);\n\n            return GetTDRACellPixelCoord(mapPoint.X, mapPoint.Y, x, y, width, height, previewSize);\n        }\n\n        /// <summary>Returns the loaded INI file of a custom map.</summary>\n        private IniFile GetCustomMapIniFile(bool loadPreviewTextureSection = true)\n        {\n            var customMapIni = new IniFile { FileName = SafePath.CombineFilePath(customMapFilePath) };\n            customMapIni.AddSection(\"Basic\");\n            customMapIni.AddSection(\"Map\");\n            customMapIni.AddSection(\"Waypoints\");\n            customMapIni.AddSection(\"ForcedOptions\");\n            customMapIni.AddSection(\"ForcedSpawnIniOptions\");\n\n            // Optionally load preview sections, to accelerate building custom map caches without reading preview.\n            if (loadPreviewTextureSection)\n            {\n                customMapIni.AddSection(\"Preview\");\n                customMapIni.AddSection(\"PreviewPack\");\n            }\n            customMapIni.AllowNewSections = false;\n            customMapIni.Parse();\n\n            return customMapIni;\n        }\n\n        /// <summary>\n        /// Loads map information from a TS/RA2 map INI file.\n        /// Returns true if successful, otherwise false.\n        /// </summary>\n        public bool InitializeFromCustomMap()\n        {\n            if (!File.Exists(customMapFilePath))\n                return false;\n\n            try\n            {\n                IniFile iniFile = GetCustomMapIniFile(loadPreviewTextureSection: false);\n\n                IniSection basicSection = iniFile.GetSection(\"Basic\");\n\n                UntranslatedName = basicSection.GetStringValue(\"Name\", \"Unnamed map\");\n                Author = basicSection.GetStringValue(\"Author\", \"Unknown author\");\n\n                string gameModesString = basicSection.GetStringValue(\"GameModes\", string.Empty);\n                if (string.IsNullOrEmpty(gameModesString))\n                {\n                    gameModesString = basicSection.GetStringValue(\"GameMode\", \"Default\");\n                }\n\n                GameModes = gameModesString.Split(',');\n\n                if (GameModes.Length == 0)\n                {\n                    Logger.Log(\"Custom map \" + customMapFilePath + \" has no game modes!\");\n                    return false;\n                }\n\n                for (int i = 0; i < GameModes.Length; i++)\n                {\n                    string gameMode = GameModes[i].Trim();\n                    GameModes[i] = gameMode.Substring(0, 1).ToUpperInvariant() + gameMode.Substring(1);\n                }\n\n                Briefing = basicSection.GetStringValue(\"Briefing\", string.Empty)\n                    .FromIniString();\n\n                CalculateSHA();\n\n                InitializeBaseSettingsFromIniSection(basicSection, isCustomMap: true);\n\n                Credits = basicSection.GetIntValue(\"Credits\", -1);\n                UnitCount = basicSection.GetIntValue(\"UnitCount\", -1);\n                NeutralHouseColor = basicSection.GetIntValue(\"NeutralColor\", -1);\n                SpecialHouseColor = basicSection.GetIntValue(\"SpecialColor\", -1);\n\n                // Initialize PreviewPath\n                {\n                    string previewPath = Path.ChangeExtension(customMapFilePath[ProgramConstants.GamePath.Length..], \".png\");\n                    if (!SafePath.GetFile(ProgramConstants.GamePath, previewPath).Exists)\n                        previewPath = null;\n\n                    PreviewPath = previewPath;\n                }\n\n                string bases = basicSection.GetStringValue(\"Bases\", string.Empty);\n                if (!string.IsNullOrEmpty(bases))\n                {\n                    Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false));\n                }\n\n                localSize = iniFile.GetStringValue(\"Map\", \"LocalSize\", \"0,0,0,0\").Split(',');\n                actualSize = iniFile.GetStringValue(\"Map\", \"Size\", \"0,0,0,0\").Split(',');\n\n                if (MainClientConstants.USE_ISOMETRIC_CELLS)\n                {\n                    localSize = iniFile.GetStringValue(\"Map\", \"LocalSize\", \"0,0,0,0\").Split(',');\n                    actualSize = iniFile.GetStringValue(\"Map\", \"Size\", \"0,0,0,0\").Split(',');\n                }\n                else\n                {\n                    x = iniFile.GetIntValue(\"Map\", \"X\", 0);\n                    y = iniFile.GetIntValue(\"Map\", \"Y\", 0);\n                    width = iniFile.GetIntValue(\"Map\", \"Width\", 0);\n                    height = iniFile.GetIntValue(\"Map\", \"Height\", 0);\n                }\n\n                for (int i = 0; i < MAX_PLAYERS; i++)\n                {\n                    string waypoint = iniFile.GetStringValue(\"Waypoints\", i.ToString(CultureInfo.InvariantCulture), string.Empty);\n\n                    if (string.IsNullOrEmpty(waypoint))\n                        break;\n\n                    waypoints.Add(waypoint);\n                }\n\n                GetTeamStartMappingPresets(basicSection);\n\n                ParseForcedOptions(iniFile, \"ForcedOptions\");\n                ParseSpawnIniOptions(iniFile, \"ForcedSpawnIniOptions\");\n\n                ExtraININame = basicSection.GetStringValueOrNull(\"ExtraININame\");\n\n                return true;\n            }\n            catch\n            {\n                Logger.Log(\"Loading custom map \" + customMapFilePath + \" failed!\");\n                return false;\n            }\n        }\n\n        // Ran after the map has been loaded from cache if it is a custom map.\n        public void AfterDeserialize(bool recalculateSHA = true)\n        {\n            if (recalculateSHA)\n            {\n                // Instead of doing so, we should just remove the Map object from cache when the map file changes.\n                // Otherwise, the metadata can be out of date.\n                Debug.Assert(false, \"The map SHA1 should not be recalculated after deserialization. Remove the Map object from cache when the map file changes instead.\");\n                CalculateSHA();\n            }\n        }\n\n        private void ParseForcedOptions(IniFile iniFile, string forcedOptionsSection)\n        {\n            List<string> keys = iniFile.GetSectionKeys(forcedOptionsSection);\n\n            if (keys == null)\n            {\n                Logger.Log(\"Invalid ForcedOptions section \\\"\" + forcedOptionsSection + \"\\\" in map \" + BaseFilePath);\n                return;\n            }\n\n            foreach (string key in keys)\n            {\n                string value = iniFile.GetStringValue(forcedOptionsSection, key, string.Empty);\n\n                if (int.TryParse(value, out int intValue))\n                {\n                    ForcedDropDownValues.Add(new KeyValuePair<string, int>(key, intValue));\n                }\n                else\n                {\n                    ForcedCheckBoxValues.Add(new KeyValuePair<string, bool>(key, Conversions.BooleanFromString(value, false)));\n                }\n            }\n        }\n\n        private void ParseSpawnIniOptions(IniFile forcedOptionsIni, string spawnIniOptionsSection)\n        {\n            List<string> spawnIniKeys = forcedOptionsIni.GetSectionKeys(spawnIniOptionsSection);\n\n            foreach (string key in spawnIniKeys)\n            {\n                ForcedSpawnIniOptions.Add(new KeyValuePair<string, string>(key,\n                    forcedOptionsIni.GetStringValue(spawnIniOptionsSection, key, string.Empty)));\n            }\n        }\n\n        public bool IsImmediatePreviewImageAvailable() => !string.IsNullOrWhiteSpace(PreviewPath) && SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).Exists;\n\n        public Image GetImmediatePreviewImage() => IsImmediatePreviewImageAvailable()\n            ? Image.Load(SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).FullName)\n            : throw new FileNotFoundException(\"Immediate preview texture not found for map \" + BaseFilePath);\n\n        public bool IsNonImmediatePreviewImageAvailable() => !string.IsNullOrWhiteSpace(customMapFilePath) && File.Exists(customMapFilePath);\n\n        public Image GetNonImmediatePreviewImage()\n        {\n            if (!IsNonImmediatePreviewImageAvailable())\n                throw new FileNotFoundException(\"Custom map file not found for map \" + BaseFilePath);\n\n            // Debug.WriteLine(\"Loading map preview from custom map INI for map \" + BaseFilePath);\n\n            return MapPreviewExtractor.ExtractMapPreview(GetCustomMapIniFile(loadPreviewTextureSection: true));\n        }\n\n        public IniFile GetMapIni()\n        {\n            Encoding mapIniEncoding = MapCodeHelper.GetMapEncoding(CompleteFilePath);\n\n            var mapIni = new IniFile(CompleteFilePath, mapIniEncoding);\n\n            if (!string.IsNullOrEmpty(ExtraININame))\n            {\n                string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, \"INI\", \"Map Code\", ExtraININame);\n                Encoding extraIniEncoding = MapCodeHelper.GetMapEncoding(extraIniPath);\n                var extraIni = new IniFile(extraIniPath, extraIniEncoding);\n                IniFile.ConsolidateIniFiles(mapIni, extraIni);\n            }\n\n            return mapIni;\n        }\n\n        public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount,\n            int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random pseudoRandom, int sideCount)\n        {\n            foreach (KeyValuePair<string, string> key in ForcedSpawnIniOptions)\n                spawnIni.SetStringValue(\"Settings\", key.Key, key.Value);\n\n            if (Credits != -1)\n                spawnIni.SetIntValue(\"Settings\", \"Credits\", Credits);\n\n            if (UnitCount != -1)\n                spawnIni.SetIntValue(\"Settings\", \"UnitCount\", UnitCount);\n\n            int neutralHouseIndex = totalPlayerCount + 1;\n            int specialHouseIndex = totalPlayerCount + 2;\n\n            if (isCoop)\n            {\n                int NextRandomSide() => pseudoRandom.Next(0, sideCount);\n\n                var allyHouses = coopInfo.AllyHouses;\n                var enemyHouses = coopInfo.EnemyHouses;\n\n                int multiId = totalPlayerCount + 1;\n                foreach (var houseInfo in allyHouses.Concat(enemyHouses))\n                {\n                    spawnIni.SetIntValue(\"HouseHandicaps\", \"Multi\" + multiId, coopDifficultyLevel);\n                    spawnIni.SetIntValue(\"HouseCountries\", \"Multi\" + multiId, houseInfo.Side == -1 ? NextRandomSide() : houseInfo.Side);\n                    spawnIni.SetIntValue(\"HouseColors\", \"Multi\" + multiId, houseInfo.Color);\n                    spawnIni.SetIntValue(\"SpawnLocations\", \"Multi\" + multiId, houseInfo.StartingLocation);\n\n                    multiId++;\n                }\n\n                for (int i = 0; i < allyHouses.Count; i++)\n                {\n                    int aMultiId = totalPlayerCount + i + 1;\n\n                    int allyIndex = 0;\n\n                    // Write alliances\n                    for (int pIndex = 0; pIndex < totalPlayerCount + allyHouses.Count; pIndex++)\n                    {\n                        int allyMultiIndex = pIndex;\n\n                        if (pIndex == aMultiId - 1)\n                            continue;\n\n                        spawnIni.SetIntValue(\"Multi\" + aMultiId + \"_Alliances\",\n                            \"HouseAlly\" + HouseAllyIndexToString(allyIndex), allyMultiIndex);\n                        spawnIni.SetIntValue(\"Multi\" + (allyMultiIndex + 1) + \"_Alliances\",\n                            \"HouseAlly\" + HouseAllyIndexToString(totalPlayerCount + i - 1), aMultiId - 1);\n                        allyIndex++;\n                    }\n                }\n\n                for (int i = 0; i < enemyHouses.Count; i++)\n                {\n                    int eMultiId = totalPlayerCount + allyHouses.Count + i + 1;\n\n                    int allyIndex = 0;\n\n                    // Write alliances\n                    for (int enemyIndex = 0; enemyIndex < enemyHouses.Count; enemyIndex++)\n                    {\n                        int allyMultiIndex = totalPlayerCount + allyHouses.Count + enemyIndex;\n\n                        if (enemyIndex == i)\n                            continue;\n\n                        spawnIni.SetIntValue(\"Multi\" + eMultiId + \"_Alliances\",\n                            \"HouseAlly\" + HouseAllyIndexToString(allyIndex), allyMultiIndex);\n                        allyIndex++;\n                    }\n                }\n\n                spawnIni.SetIntValue(\"Settings\", \"AIPlayers\",\n                    aiPlayerCount + allyHouses.Count + enemyHouses.Count);\n\n                neutralHouseIndex += allyHouses.Count + enemyHouses.Count;\n                specialHouseIndex += allyHouses.Count + enemyHouses.Count;\n            }\n\n            if (NeutralHouseColor > -1)\n                spawnIni.SetIntValue(\"HouseColors\", \"Multi\" + neutralHouseIndex, NeutralHouseColor);\n\n            if (SpecialHouseColor > -1)\n                spawnIni.SetIntValue(\"HouseColors\", \"Multi\" + specialHouseIndex, SpecialHouseColor);\n\n            if (Bases > -1)\n                spawnIni.SetBooleanValue(\"Settings\", \"Bases\", Convert.ToBoolean(Bases));\n        }\n\n        private static string HouseAllyIndexToString(int index)\n        {\n            string[] houseAllyIndexStrings = new string[]\n            {\n                \"One\",\n                \"Two\",\n                \"Three\",\n                \"Four\",\n                \"Five\",\n                \"Six\",\n                \"Seven\"\n            };\n\n            return houseAllyIndexStrings[index];\n        }\n\n        public string GetSizeString()\n        {\n            if (MainClientConstants.USE_ISOMETRIC_CELLS)\n            {\n                if (actualSize == null || actualSize.Length < 4)\n                    return \"Not available\";\n\n                return actualSize[2] + \"x\" + actualSize[3];\n            }\n            else\n            {\n                return width + \"x\" + height;\n            }\n        }\n\n        private static Point GetTDRAWaypointCoords(string waypoint, int x, int y, int width, int height, Point previewSizePoint)\n        {\n            int waypointCoordsInt = Conversions.IntFromString(waypoint, -1);\n\n            if (waypointCoordsInt < 0)\n                return new Point(0, 0);\n\n            // https://modenc.renegadeprojects.com/Waypoints\n            int waypointX = waypointCoordsInt % MainClientConstants.TDRA_WAYPOINT_COEFFICIENT;\n            int waypointY = waypointCoordsInt / MainClientConstants.TDRA_WAYPOINT_COEFFICIENT;\n\n            return GetTDRACellPixelCoord(waypointX, waypointY, x, y, width, height, previewSizePoint);\n        }\n\n        private static Point GetTDRACellPixelCoord(int cellX, int cellY, int x, int y, int width, int height, Point previewSizePoint)\n        {\n            int rx = cellX - x;\n            int ry = cellY - y;\n\n            double ratioX = rx / (double)width;\n            double ratioY = ry / (double)height;\n\n            int pixelX = (int)(ratioX * previewSizePoint.X);\n            int pixelY = (int)(ratioY * previewSizePoint.Y);\n\n            return new Point(pixelX, pixelY);\n        }\n\n        /// <summary>\n        /// Converts a waypoint's coordinate string into pixel coordinates on the preview image.\n        /// </summary>\n        /// <returns>The waypoint's location on the map preview as a point.</returns>\n        private static Point GetIsometricWaypointCoords(string waypoint, string[] actualSizeValues, string[] localSizeValues,\n            Point previewSizePoint)\n        {\n            string[] parts = waypoint.Split(',');\n\n            int xCoordIndex = parts[0].Length - 3;\n\n            int isoTileY = Convert.ToInt32(parts[0].Substring(0, xCoordIndex), CultureInfo.InvariantCulture);\n            int isoTileX = Convert.ToInt32(parts[0].Substring(xCoordIndex), CultureInfo.InvariantCulture);\n\n            int level = 0;\n\n            if (parts.Length > 1)\n                level = Conversions.IntFromString(parts[1], 0);\n\n            return GetIsoTilePixelCoord(isoTileX, isoTileY, actualSizeValues, localSizeValues, previewSizePoint, level);\n        }\n\n        private static Point GetIsoTilePixelCoord(int isoTileX, int isoTileY, string[] actualSizeValues, string[] localSizeValues, Point previewSizePoint, int level)\n        {\n            int rx = isoTileX - isoTileY + Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1;\n            int ry = isoTileX + isoTileY - Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1;\n\n            int pixelPosX = rx * MainClientConstants.MAP_CELL_SIZE_X / 2;\n            int pixelPosY = ry * MainClientConstants.MAP_CELL_SIZE_Y / 2 - level * MainClientConstants.MAP_CELL_SIZE_Y / 2;\n\n            pixelPosX = pixelPosX - (Convert.ToInt32(localSizeValues[0], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X);\n            pixelPosY = pixelPosY - (Convert.ToInt32(localSizeValues[1], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y);\n\n            // Calculate map size\n            int mapSizeX = Convert.ToInt32(localSizeValues[2], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X;\n            int mapSizeY = Convert.ToInt32(localSizeValues[3], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y;\n\n            double ratioX = Convert.ToDouble(pixelPosX) / mapSizeX;\n            double ratioY = Convert.ToDouble(pixelPosY) / mapSizeY;\n\n            int pixelX = Convert.ToInt32(ratioX * previewSizePoint.X);\n            int pixelY = Convert.ToInt32(ratioY * previewSizePoint.Y);\n\n            return new Point(pixelX, pixelY);\n        }\n\n        /// <summary>\n        /// Opens the folder containing this map in the system file manager and selects the map file.\n        /// </summary>\n        public void OpenContainingFolder()\n        {\n            FileInfo mapFileInfo = SafePath.GetFile(CompleteFilePath);\n            if (!mapFileInfo.Exists)\n                return;\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                // https://stackoverflow.com/questions/13680415/how-to-open-explorer-with-a-specific-file-selected\n                ProcessLauncher.StartShellProcess(\"explorer.exe\", $\"/select,\\\"{mapFileInfo.FullName}\\\"\");\n            }\n            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n            {\n                // https://stackoverflow.com/questions/39214539/opening-finder-from-terminal-with-file-selected\n                ProcessLauncher.StartShellProcess(\"open\", $\"-R \\\"{mapFileInfo.FullName}\\\"\");\n            }\n            else\n            {\n                // Linux: no standard way to select a file, just open the folder\n                ProcessLauncher.StartShellProcess(mapFileInfo.Directory?.FullName);\n            }\n        }\n\n        public override bool Equals(object other)\n        {\n            if (other is Map otherMap)\n            {\n                Debug.Assert(otherMap?.SHA1 != null || SHA1 != null);\n                return string.Equals(SHA1, otherMap?.SHA1, StringComparison.InvariantCultureIgnoreCase);\n            }\n\n            return false;\n        }\n\n        public override int GetHashCode() => SHA1 != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(SHA1) : 0;\n\n        public static bool operator ==(Map left, Map right) => left is null ? right is null : left.Equals(right);\n\n        public static bool operator !=(Map left, Map right) => !(left == right);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapChangeEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Domain.Multiplayer;\npublic class MapChangedEventArgs : EventArgs\n{\n    public Map Map { get; set; }\n    public MapChangeType ChangeType { get; set; }\n    public string PreviousMapSHA1 { get; set; }\n\n    public MapChangedEventArgs(Map map, MapChangeType changeType, string previousMapSHA1 = null)\n    {\n        Map = map;\n        ChangeType = changeType;\n        PreviousMapSHA1 = previousMapSHA1;\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapFileEventArgs.cs",
    "content": "﻿using System;\nusing System.IO;\n\nnamespace DTAClient.Domain.Multiplayer;\npublic class MapFileEventArgs : EventArgs\n{\n    public string FilePath { get; set; }\n    public string FileName { get; set; }\n    public WatcherChangeTypes ChangeType { get; set; }\n    public string OldFilePath { get; set; }\n\n    public MapFileEventArgs(string filePath, WatcherChangeTypes changeType, string oldFilePath = null)\n    {\n        FilePath = filePath;\n        FileName = Path.GetFileNameWithoutExtension(filePath);\n        ChangeType = changeType;\n        OldFilePath = oldFilePath;\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapFileWatcher.cs",
    "content": "using System;\nusing System.IO;\nusing Rampastring.Tools;\n\nnamespace DTAClient.Domain.Multiplayer;\npublic class MapFileWatcher\n{\n    private readonly string mapsDirectory;\n    private readonly string mapFileExtension;\n    private FileSystemWatcher fileSystemWatcher;\n\n    public event EventHandler<MapFileEventArgs> MapFileChanged;\n\n    public MapFileWatcher(string mapsPath, string fileExtension)\n    {\n        mapsDirectory = mapsPath;\n        mapFileExtension = fileExtension;\n    }\n\n    public void StartWatching()\n    {\n        if (fileSystemWatcher != null)\n            return;\n\n        DirectoryInfo directoryInfo = SafePath.GetDirectory(mapsDirectory);\n        if (!directoryInfo.Exists)\n            return;\n\n        try\n        {\n            fileSystemWatcher = new FileSystemWatcher(mapsDirectory, $\"*.{mapFileExtension}\")\n            {\n                NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size,\n                IncludeSubdirectories = true\n            };\n\n            fileSystemWatcher.Created += OnFileSystemEvent;\n            fileSystemWatcher.Changed += OnFileSystemEvent;\n            fileSystemWatcher.Deleted += OnFileSystemEvent;\n            fileSystemWatcher.Renamed += OnFileRenamed;\n\n            fileSystemWatcher.EnableRaisingEvents = true;\n\n            Logger.Log($\"MapFileWatcher: Started watching {mapsDirectory} for *.{mapFileExtension} files\");\n        }\n        catch (Exception ex)\n        {\n            Logger.Log($\"MapFileWatcher: Failed to start watching directory {mapsDirectory}: {ex.Message}\");\n            fileSystemWatcher?.Dispose();\n            fileSystemWatcher = null;\n        }\n    }\n\n    private void OnFileSystemEvent(object sender, FileSystemEventArgs e)\n    {\n        ProcessFileEvent(e.FullPath, e.ChangeType);\n    }\n\n    private void OnFileRenamed(object sender, RenamedEventArgs e)\n    {\n        // delete + create\n        ProcessFileEvent(e.OldFullPath, WatcherChangeTypes.Deleted);\n        ProcessFileEvent(e.FullPath, WatcherChangeTypes.Created);\n    }\n\n    private void ProcessFileEvent(string filePath, WatcherChangeTypes changeType)\n    {\n        try\n        {\n            var eventArgs = new MapFileEventArgs(filePath, changeType);\n            MapFileChanged?.Invoke(this, eventArgs);\n        }\n        catch (Exception ex)\n        {\n            Logger.Log($\"MapFileWatcher: Error processing file event for {filePath}: {ex.Message}\");\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapLoader.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nusing ClientCore;\nusing ClientCore.Extensions;\n\nusing Rampastring.Tools;\n\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public enum MapChangeType\n    {\n        Added,\n        Updated,\n        Removed\n    }\n\n    public class MapLoader : IDisposable\n    {\n        private const string CUSTOM_MAPS_DIRECTORY = \"Maps/Custom\";\n\n        private const int CurrentCustomMapCacheVersion = 5;\n\n        private static string GetCustomMapCacheFileName(int version) => version == 1 ? \"custom_map_cache\" : $\"custom_map_cache_v{version}\";\n\n        private static readonly string CUSTOM_MAPS_CACHE = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, GetCustomMapCacheFileName(CurrentCustomMapCacheVersion));\n        private static readonly IReadOnlyList<string> LEGACY_CUSTOM_MAP_CACHE_FILES = Enumerable.Range(0, CurrentCustomMapCacheVersion)\n            .Select(version => SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, GetCustomMapCacheFileName(version)))\n            .ToList();\n\n        private const string MultiMapsSection = \"MultiMaps\";\n        private const string GameModesSection = \"GameModes\";\n        private const string GameModeAliasesSection = \"GameModeAliases\";\n        private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { IncludeFields = true };\n        private MapFileWatcher mapFileWatcher;\n        private readonly object mapModificationLock = new object();\n        private const int _mapChangeRetryCount = 3;\n\n        private readonly List<GameMode> _gameModes = [];\n\n        /// <summary>\n        /// List of game modes.\n        /// </summary>\n        public IReadOnlyList<GameMode> GameModes => _gameModes;\n\n        private GameModeMapCollection _gameModeMaps;\n        public IReadOnlyGameModeMapCollection GameModeMaps => _gameModeMaps;\n\n        /// <summary>\n        /// An event that is fired when the maps have been loaded.\n        /// </summary>\n        public event EventHandler MapLoadingComplete;\n\n        /// <summary>\n        /// Fired when a map file is added, updated, or removed.\n        /// </summary>\n        public event EventHandler<MapChangedEventArgs> MapChanged;\n\n        /// <summary>\n        /// A list of game mode aliases.\n        /// Every game mode entry that exists in this dictionary will get\n        /// replaced by the game mode entries of the value string array\n        /// when map is added to game mode map lists.\n        /// </summary>\n        private Dictionary<string, string[]> GameModeAliases = new Dictionary<string, string[]>();\n\n        private Dictionary<string, string> _translatedMapNames = new();\n\n        /// <summary>\n        /// A dictionary of translated map names. Used to look up the \n        /// translated name of a map without knowing the ID of the map.\n        /// </summary>\n        public IReadOnlyDictionary<string, string> TranslatedMapNames => _translatedMapNames;\n\n        /// <summary>\n        /// List of gamemodes allowed to be used on custom maps in order for them to display in map list.\n        /// </summary>\n        private string[] AllowedGameModes = ClientConfiguration.Instance.AllowedCustomGameModes.Split(',');\n\n        public const int MapPreviewCacheCapacity = 100;\n\n        private readonly IMapPreviewCacheManager mapPreviewCacheManager = new MapPreviewCacheManager(capacity: MapPreviewCacheCapacity);\n\n        public MapLoader() { }\n\n        public void Initialize()\n        {\n            MapLoadingComplete += (sender, args) => StartMapFileWatcher();\n        }\n\n        /// <summary>\n        /// Sets up file watching for maps.\n        /// </summary>\n        public void StartMapFileWatcher()\n        {\n            if (mapFileWatcher != null)\n                return;\n\n            string customMapsPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY);\n\n            mapFileWatcher = new MapFileWatcher(customMapsPath, ClientConfiguration.Instance.MapFileExtension);\n            mapFileWatcher.MapFileChanged += OnMapFileChanged;\n            mapFileWatcher.StartWatching();\n        }\n\n        /// <summary>\n        /// Asynchronously loads maps based on INI info as well as those in the custom maps directory.\n        /// </summary>\n        public Task LoadMapsAsync() => Task.Run(LoadMapsInternalAsync);\n\n        private async Task LoadMapsInternalAsync()\n        {\n            Logger.Log(\"MapLoader: Map loading task started.\");\n            var stopwatch = Stopwatch.StartNew();\n\n            string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath);\n\n            Logger.Log($\"MapLoader: Loading maps from {mpMapsPath}.\");\n\n            IniFile mpMapsIni = new IniFile(mpMapsPath);\n\n            LoadGameModes(mpMapsIni);\n            LoadGameModeAliases(mpMapsIni);\n            // 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.\n            await LoadMultiMapsAsync(mpMapsIni);\n            await LoadCustomMapsAsync();\n\n            Logger.Log(\"MapLoader: Post-processing game mode map collections.\");\n            _gameModes.RemoveAll(g => g.Maps.Count < 1);\n            _gameModeMaps = new GameModeMapCollection(_gameModes);\n\n            // Clean up any name-based favorite entries after migration (legacy: changed from name to sha1)\n            CleanupMigratedFavorites();\n\n            stopwatch.Stop();\n\n            Logger.Log($\"MapLoader: Map loading complete. Total time: {stopwatch.ElapsedMilliseconds} ms\");\n            MapLoadingComplete?.Invoke(this, EventArgs.Empty);\n        }\n\n        private async void OnMapFileChanged(object sender, MapFileEventArgs e)\n        {\n            switch (e.ChangeType)\n            {\n                case WatcherChangeTypes.Created:\n                    await HandleMapFileAdded(e.FilePath);\n                    break;\n                case WatcherChangeTypes.Changed:\n                    await HandleMapFileChanged(e.FilePath);\n                    break;\n                case WatcherChangeTypes.Deleted:\n                    await HandleMapFileDeleted(e.FilePath);\n                    break;\n            }\n        }\n\n        private async Task HandleMapFileAdded(string filePath)\n        {\n            try\n            {\n                if (!File.Exists(filePath))\n                    return;\n\n                string baseFilePath = GetBaseFilePathFromFullPath(filePath);\n                if (string.IsNullOrEmpty(baseFilePath))\n                    return;\n\n                // If, for instance, the file was just extracted, the program that created it may still\n                // have a lock on the file. Retry a couple of times.\n                Map map = null;\n                bool success = false;\n\n                for (int attempt = 0; attempt < _mapChangeRetryCount; attempt++)\n                {\n                    try\n                    {\n                        map = new Map(baseFilePath, true);\n                        if (map.InitializeFromCustomMap())\n                        {\n                            success = true;\n                            break;\n                        }\n                    }\n                    catch (IOException)\n                    {\n                        if (attempt < _mapChangeRetryCount - 1)\n                            await Task.Delay(100);\n                        else\n                            throw;\n                    }\n                }\n\n                if (success && map != null)\n                {\n                    lock (mapModificationLock)\n                    {\n                        if (IsMapAlreadyLoaded(map.SHA1))\n                            return;\n\n                        AddMapToGameModes(map, true);\n                        UpdateGameModeMaps();\n\n                        Logger.Log($\"MapLoader: Added new map {map.Name} from {filePath}\");\n                        MapChanged?.Invoke(this, new MapChangedEventArgs(map, MapChangeType.Added));\n                    }\n                }\n                else\n                {\n                    Logger.Log($\"MapLoader: Failed to load map info from {filePath}\");\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"MapLoader: Error adding map from {filePath}: {ex.Message}\");\n            }\n        }\n\n        private async Task HandleMapFileChanged(string filePath)\n        {\n            try\n            {\n                string baseFilePath = GetBaseFilePathFromFullPath(filePath);\n                if (string.IsNullOrEmpty(baseFilePath))\n                    return;\n\n                // If editing a map, the program that saved the new version may still\n                // have a lock on the file. Retry a couple of times.\n                Map newMap = null;\n                bool success = false;\n\n                for (int attempt = 0; attempt < _mapChangeRetryCount; attempt++)\n                {\n                    try\n                    {\n                        newMap = new Map(baseFilePath, true);\n                        if (newMap.InitializeFromCustomMap())\n                        {\n                            success = true;\n                            break;\n                        }\n                    }\n                    catch (IOException)\n                    {\n                        if (attempt < _mapChangeRetryCount - 1)\n                            await Task.Delay(100);\n                        else\n                            throw;\n                    }\n                }\n\n                if (success && newMap != null)\n                {\n                    lock (mapModificationLock)\n                    {\n                        string oldSHA1 = FindMapSHA1ByFilePath(baseFilePath);\n\n                        if (!string.IsNullOrEmpty(oldSHA1))\n                        {\n                            if (oldSHA1 != newMap.SHA1)\n                            {\n                                // SHA1 changed, remove old and add new\n                                RemoveMapBySHA1(oldSHA1);\n                                AddMapToGameModes(newMap, true);\n                                UpdateGameModeMaps();\n\n                                Logger.Log($\"MapLoader: Updated map {newMap.Name} from {filePath} (SHA1 changed: {oldSHA1} -> {newMap.SHA1})\");\n                                MapChanged?.Invoke(this, new MapChangedEventArgs(newMap, MapChangeType.Updated, oldSHA1));\n                            }\n                            else\n                            {\n                                Logger.Log($\"MapLoader: Map file {filePath} changed but SHA1 remained the same ({newMap.SHA1})\");\n                            }\n                        }\n                        else\n                        {\n                            // Map not found, treat as new\n                            Logger.Log($\"MapLoader: Changed event for unknown map {filePath}, treating as new\");\n                            AddMapToGameModes(newMap, true);\n                            UpdateGameModeMaps();\n                            MapChanged?.Invoke(this, new MapChangedEventArgs(newMap, MapChangeType.Added));\n                        }\n                    }\n                }\n                else\n                {\n                    Logger.Log($\"MapLoader: Failed to reload map info from {filePath}\");\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"MapLoader: Error updating map from {filePath}: {ex.Message}\");\n            }\n        }\n\n        private async Task HandleMapFileDeleted(string filePath)\n        {\n            try\n            {\n                string baseFilePath = GetBaseFilePathFromFullPath(filePath);\n                if (string.IsNullOrEmpty(baseFilePath))\n                    return;\n\n                lock (mapModificationLock)\n                {\n                    string mapSHA1 = FindMapSHA1ByFilePath(baseFilePath);\n\n                    if (!string.IsNullOrEmpty(mapSHA1))\n                    {\n                        var removedMap = FindMapBySHA1(mapSHA1);\n                        RemoveMapBySHA1(mapSHA1);\n                        UpdateGameModeMaps();\n\n                        Logger.Log($\"MapLoader: Removed map from {filePath}\");\n                        if (removedMap != null)\n                            MapChanged?.Invoke(this, new MapChangedEventArgs(removedMap, MapChangeType.Removed));\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"MapLoader: Error removing map from {filePath}: {ex.Message}\");\n            }\n        }\n\n        /// <summary>\n        /// Converts a full file path to the base file path used by the map system.\n        /// C:\\YR\\Maps\\Custom\\abc123.map > Maps\\Custom\\abc123\n        /// </summary>\n        private string GetBaseFilePathFromFullPath(string fullPath)\n        {\n            try\n            {\n                string gamePathNormalized = Path.GetFullPath(ProgramConstants.GamePath);\n                string fullPathNormalized = Path.GetFullPath(fullPath);\n\n                if (!fullPathNormalized.StartsWith(gamePathNormalized, StringComparison.OrdinalIgnoreCase))\n                    return null;\n\n                string relativePath = fullPathNormalized.Substring(gamePathNormalized.Length);\n                if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString())\n                    || relativePath.StartsWith(Path.AltDirectorySeparatorChar.ToString()))\n                {\n                    relativePath = relativePath.Substring(1);\n                }\n\n                string baseFilePath = relativePath.Substring(0, relativePath.Length - Path.GetExtension(relativePath).Length);\n\n                return baseFilePath.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/');\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"MapLoader: Error converting file path {fullPath}: {ex.Message}\");\n                return null;\n            }\n        }\n\n        private bool IsMapAlreadyLoaded(string sha1)\n            => GameModes.SelectMany(gm => gm.Maps).Any(map => map.SHA1 == sha1);\n\n        private Map FindMapBySHA1(string sha1)\n            => GameModes.SelectMany(gm => gm.Maps).FirstOrDefault(map => map.SHA1 == sha1);\n\n        private string FindMapSHA1ByFilePath(string baseFilePath)\n            => GameModes.SelectMany(gm => gm.Maps)\n                .Where(map => !map.Official && map.BaseFilePath.Equals(baseFilePath, StringComparison.OrdinalIgnoreCase))\n                .FirstOrDefault()?.SHA1;\n\n        private void RemoveMapBySHA1(string sha1)\n        {\n            foreach (var gameMode in GameModes)\n                gameMode.Maps.RemoveAll(map => map.SHA1 == sha1);\n        }\n\n        private void UpdateGameModeMaps()\n        {\n            _gameModes.RemoveAll(g => g.Maps.Count < 1);\n            _gameModeMaps = new GameModeMapCollection(_gameModes);\n        }\n\n        private async Task LoadMultiMapsAsync(IniFile mpMapsIni)\n        {\n            List<string> keys = mpMapsIni.GetSectionKeys(MultiMapsSection);\n\n            if (keys == null)\n            {\n                Logger.Log(\"Loading multiplayer map list failed!!!\");\n                return;\n            }\n\n            Task<Map>[] tasks = keys.Select(key => Task.Run(() =>\n            {\n                try\n                {\n                    string mapFilePathValue = mpMapsIni.GetStringValue(MultiMapsSection, key, string.Empty);\n                    string mapFilePath = SafePath.CombineFilePath(mapFilePathValue);\n                    FileInfo mapFile = SafePath.GetFile(ProgramConstants.GamePath, FormattableString.Invariant($\"{mapFilePath}.{ClientConfiguration.Instance.MapFileExtension}\"));\n\n                    if (!mapFile.Exists)\n                    {\n                        Logger.Log(\"Map \" + mapFile.FullName + \" doesn't exist!\");\n                        return null;\n                    }\n\n                    var map = new Map(mapFilePathValue, false);\n                    if (!map.InitializeFromMpMapsINI(mpMapsIni))\n                        return null;\n\n                    return map;\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"Error loading map for key {key}: {ex}\");\n                    return null;\n                }\n            })).ToArray();\n\n            Task waitMultiMapsTask = Task.WhenAll(tasks);\n            while (await Task.WhenAny(waitMultiMapsTask, Task.Delay(1000)) != waitMultiMapsTask)\n            {\n                string message = \"MapLoader: Waiting for the multiplayer map loading task to complete. Remaining files: \" + tasks.Count(t => !t.IsCompleted) + \". Total: \" + tasks.Length;\n                Debug.WriteLine(message);\n                Logger.Log(message);\n            }\n\n            await waitMultiMapsTask;\n\n            foreach (Map map in tasks.Select(t => t.Result).Where(m => m != null))\n            {\n                AddMapToGameModes(map, false);\n                _translatedMapNames[map.UntranslatedName] = map.Name;\n            }\n        }\n\n        private void LoadGameModes(IniFile mpMapsIni)\n        {\n            var gameModes = mpMapsIni.GetSectionKeys(GameModesSection);\n            if (gameModes != null)\n            {\n                foreach (string key in gameModes)\n                {\n                    string gameModeName = mpMapsIni.GetStringValue(GameModesSection, key, string.Empty);\n                    if (!string.IsNullOrEmpty(gameModeName))\n                    {\n                        GameMode gm = new GameMode(gameModeName);\n                        _gameModes.Add(gm);\n                    }\n                }\n            }\n        }\n\n        private void LoadGameModeAliases(IniFile mpMapsIni)\n        {\n            var gmAliases = mpMapsIni.GetSectionKeys(GameModeAliasesSection);\n\n            if (gmAliases != null)\n            {\n                foreach (string key in gmAliases)\n                {\n                    GameModeAliases.Add(key, mpMapsIni.GetStringValue(GameModeAliasesSection, key, string.Empty).Split(\n                        new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));\n                }\n            }\n        }\n\n        private async Task LoadCustomMapsAsync()\n        {\n            DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY);\n\n            if (!customMapsDirectory.Exists)\n            {\n                Logger.Log($\"Custom maps directory {customMapsDirectory} does not exist!\");\n                return;\n            }\n\n            Logger.Log(\"MapLoader: Loading custom maps...\");\n\n            // Load custom map cache from file system\n            Stopwatch stopwatch = Stopwatch.StartNew();\n\n            IEnumerable<FileInfo> mapFiles = customMapsDirectory.EnumerateFiles($\"*.{ClientConfiguration.Instance.MapFileExtension}\");\n\n            // Note: using synchronous file I/O here saves a noticeable amount of latency compared to async.\n            CustomMapCache customMapCache = LoadCustomMapCache();\n\n            stopwatch.Stop();\n            Logger.Log(FormattableString.Invariant($\"MapLoader: Loaded custom map cache from file system in {stopwatch.ElapsedMilliseconds} ms\"));\n\n            // Process uncached custom maps.\n            stopwatch.Restart();\n\n            List<string> localMapPaths;\n            {\n                int mapFileExtensionWithDotLength = $\".{ClientConfiguration.Instance.MapFileExtension}\".Length;\n\n                Task<string>[] tasks = mapFiles.Select(mapFile => Task.Run(() =>\n                {\n                    string baseFilePath = mapFile.FullName.Substring(ProgramConstants.GamePath.Length);\n                    baseFilePath = baseFilePath.Substring(0, baseFilePath.Length - mapFileExtensionWithDotLength);\n\n                    string normalizedPath = baseFilePath\n                        .Replace(Path.DirectorySeparatorChar, '/')\n                        .Replace(Path.AltDirectorySeparatorChar, '/');\n\n                    if (customMapCache.Items.TryGetValue(normalizedPath, out var cachedItem) && !cachedItem.IsOutdated())\n                    {\n                        // Use cached map\n                        return normalizedPath;\n                    }\n\n                    // Not in cache or outdated\n                    var map = new Map(normalizedPath, true);\n                    if (map.InitializeFromCustomMap())\n                        customMapCache.Items[normalizedPath] = new CustomMapCache.Item(map);\n\n                    return normalizedPath;\n                })).ToArray();\n\n                Task waitCustomMapsTask = Task.WhenAll(tasks);\n                while (await Task.WhenAny(waitCustomMapsTask, Task.Delay(1000)) != waitCustomMapsTask)\n                {\n                    string message = \"MapLoader: Waiting for the custom map loading task to complete. Remaining files: \" + tasks.Count(t => !t.IsCompleted) + \". Total: \" + tasks.Length;\n                    Debug.WriteLine(message);\n                    Logger.Log(message);\n                }\n\n                await waitCustomMapsTask;\n\n                localMapPaths = tasks.Select(t => t.Result).ToList();\n            }\n\n            stopwatch.Stop();\n            Logger.Log(FormattableString.Invariant($\"MapLoader: Processed uncached custom maps in {stopwatch.ElapsedMilliseconds} ms\"));\n\n            // Remove cached maps that no longer exist locally\n            stopwatch.Restart();\n\n            HashSet<string> missingMapPaths;\n            {\n                HashSet<string> cachedMapPaths = customMapCache.Items.Keys.ToHashSet();\n                cachedMapPaths.ExceptWith(localMapPaths);\n                missingMapPaths = cachedMapPaths;\n            }\n\n            foreach (string missingPath in missingMapPaths)\n                customMapCache.Items.TryRemove(missingPath, out _);\n\n            stopwatch.Stop();\n            Logger.Log(FormattableString.Invariant($\"MapLoader: Removed outdated maps from cache in {stopwatch.ElapsedMilliseconds} ms\"));\n\n            // Save custom map cache\n            stopwatch.Restart();\n            CacheCustomMaps(customMapCache);\n            stopwatch.Stop();\n            Logger.Log(FormattableString.Invariant($\"MapLoader: Saved custom map cache to disk in {stopwatch.ElapsedMilliseconds} ms\"));\n\n            foreach (Map map in customMapCache.Items.Values.Select(item => item.Map))\n            {\n                AddMapToGameModes(map, false);\n            }\n\n            Logger.Log(\"MapLoader: Custom maps loaded.\");\n        }\n\n        /// <summary>\n        /// Save cache of custom maps.\n        /// </summary>\n        /// <param name=\"customMapCache\">Custom maps to cache</param>\n        private void CacheCustomMaps(CustomMapCache customMapCache)\n        {\n            var jsonData = JsonSerializer.Serialize(customMapCache, jsonSerializerOptions);\n\n            File.WriteAllText(CUSTOM_MAPS_CACHE, jsonData);\n        }\n\n        /// <summary>\n        /// Load previously cached custom maps\n        /// </summary>\n        /// <returns></returns>\n        private CustomMapCache LoadCustomMapCache()\n        {\n            // Delete any legacy cache files\n            foreach (string legacyCacheFile in LEGACY_CUSTOM_MAP_CACHE_FILES.Where(File.Exists))\n            {\n                try\n                {\n                    File.Delete(legacyCacheFile);\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log($\"Failed to delete legacy custom map cache file {legacyCacheFile}: {ex.Message}\");\n                }\n            }\n\n            // Load current cache\n            try\n            {\n                var jsonData = File.ReadAllText(CUSTOM_MAPS_CACHE);\n\n                var customMapCache = JsonSerializer.Deserialize<CustomMapCache>(jsonData, jsonSerializerOptions);\n\n                if (customMapCache?.Version != CurrentCustomMapCacheVersion)\n                    return new CustomMapCache() { Version = CurrentCustomMapCacheVersion, Items = [] };\n\n                foreach (CustomMapCache.Item customMap in customMapCache.Items.Values)\n                    customMap.Map.AfterDeserialize(recalculateSHA: false);\n\n                // Remove outdated items\n                foreach (var mapPath in customMapCache.Items.Keys.ToList())\n                {\n                    if (customMapCache.Items[mapPath].IsOutdated())\n                    {\n                        customMapCache.Items.TryRemove(mapPath, out _);\n                    }\n                }\n\n                return customMapCache;\n            }\n            catch (Exception)\n            {\n                return new CustomMapCache() { Version = CurrentCustomMapCacheVersion, Items = [] };\n            }\n        }\n\n        /// <summary>\n        /// Attempts to load a custom map.\n        /// </summary>\n        /// <param name=\"mapPath\">The path to the map file relative to the game directory.</param>\n        /// <param name=\"resultMessage\">When method returns, contains a message reporting whether or not loading the map failed and how.</param>\n        /// <returns>The map if loading it was successful, otherwise false.</returns>\n        public Map LoadCustomMap(string mapPath, out string resultMessage)\n        {\n            Debug.Assert(!mapPath.EndsWith($\".{ClientConfiguration.Instance.MapFileExtension}\", StringComparison.InvariantCultureIgnoreCase), $\"Unexpected map path {mapPath}. It should not end with the map extension.\");\n\n            if (mapPath != mapPath.ToWin32FileName())\n            {\n                Logger.Log(\"LoadCustomMap: Map \" + FormattableString.Invariant($\"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}\") + \" contains WIN32API reserved characters!\");\n\n                // Return \"map file does not exist\" message to hide technical details towards users\n                resultMessage = string.Format(\"Map file {0} doesn't exist!\".L10N(\"Client:MapLoader:MapFileDoesNotExist\"), FormattableString.Invariant($\"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}\"));\n\n                return null;\n            }\n\n            string customMapFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($\"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}\"));\n            FileInfo customMapFile = SafePath.GetFile(customMapFilePath);\n\n            if (!customMapFile.Exists)\n            {\n                Logger.Log(\"LoadCustomMap: Map \" + customMapFile.FullName + \" not found!\");\n                resultMessage = string.Format(\"Map file {0} doesn't exist!\".L10N(\"Client:MapLoader:MapFileDoesNotExist\"), customMapFile.Name);\n\n                return null;\n            }\n\n            Logger.Log(\"LoadCustomMap: Loading custom map \" + customMapFile.FullName);\n\n            var map = new Map(mapPath, true);\n\n            if (map.InitializeFromCustomMap())\n            {\n                foreach (GameMode gm in GameModes)\n                {\n                    if (gm.Maps.Find(m => m.SHA1 == map.SHA1) != null)\n                    {\n                        Logger.Log(\"LoadCustomMap: Custom map \" + customMapFile.FullName + \" is already loaded!\");\n                        resultMessage = string.Format(\"Map {0} is already loaded.\".L10N(\"Client:MapLoader:MapAlreadyLoaded\"), map.Name);\n\n                        return null;\n                    }\n                }\n\n                Logger.Log(\"LoadCustomMap: Map \" + customMapFile.FullName + \" added successfully.\");\n\n                AddMapToGameModes(map, true);\n                var gameModes = GameModes.Where(gm => gm.Maps.Contains(map));\n                _gameModeMaps.AddRange(gameModes.Select(gm => new GameModeMap(gm, map, false)));\n\n                resultMessage = string.Format(\"Map {0} loaded successfully.\".L10N(\"Client:MapLoader:MapLoadedSuccessfully\"), map.Name);\n\n                return map;\n            }\n\n            Logger.Log(\"LoadCustomMap: Loading map \" + customMapFile.FullName + \" failed!\");\n            resultMessage = string.Format(\"Loading map {0} failed!\".L10N(\"Client:MapLoader:MapLoadingFailed\"), Path.GetFileNameWithoutExtension(customMapFile.Name));\n\n            return null;\n        }\n\n        public void DeleteCustomMap(GameModeMap gameModeMap)\n        {\n            Logger.Log(\"Deleting map \" + gameModeMap.Map.UntranslatedName);\n            File.Delete(gameModeMap.Map.CompleteFilePath);\n            foreach (GameMode gameMode in GameModeMaps.GameModes)\n            {\n                gameMode.Maps.Remove(gameModeMap.Map);\n            }\n\n            _gameModeMaps.Remove(gameModeMap);\n        }\n\n        /// <summary>\n        /// Adds map to all eligible game modes.\n        /// </summary>\n        /// <param name=\"map\">Map to add.</param>\n        /// <param name=\"enableLogging\">If set to true, a message for each game mode the map is added to is output to the log file.</param>\n        private void AddMapToGameModes(Map map, bool enableLogging)\n        {\n            foreach (string gameMode in map.GameModes)\n            {\n                if (!GameModeAliases.TryGetValue(gameMode, out string[] gameModeAliases))\n                    gameModeAliases = new string[] { gameMode };\n\n                foreach (string gameModeAlias in gameModeAliases)\n                {\n                    if (!map.Official && !(AllowedGameModes.Contains(gameMode) || AllowedGameModes.Contains(gameModeAlias)))\n                        continue;\n\n                    GameMode gm = GameModes.FirstOrDefault(g => g.Name == gameModeAlias);\n                    if (gm == null)\n                    {\n                        gm = new GameMode(gameModeAlias);\n                        _gameModes.Add(gm);\n                    }\n\n                    gm.Maps.Add(map);\n                    if (enableLogging)\n                        Logger.Log(\"AddMapToGameModes: Added map \" + map.UntranslatedName + \" to game mode \" + gm.Name);\n                }\n            }\n        }\n\n        /// <summary>\n        /// Removes any name-based favorite entries that have been successfully migrated to SHA1.\n        /// This runs after all maps have been processed to ensure complete migration.\n        /// </summary>\n        private void CleanupMigratedFavorites()\n        {\n            var favoriteMaps = UserINISettings.Instance.FavoriteMaps;\n            if (favoriteMaps == null || !favoriteMaps.Any())\n                return;\n\n            var entriesToRemove = new List<string>();\n\n            foreach (string favoriteKey in favoriteMaps)\n            {\n                string[] parts = favoriteKey.Split(':');\n                if (parts.Length != 2)\n                    continue;\n\n                string mapName = parts[0];\n                string gameModeName = parts[1];\n\n                // Check if there's a corresponding SHA1-based entry for any map with this name\n                var gameMode = GameModes.FirstOrDefault(gm => gm.Name == gameModeName);\n                if (gameMode != null)\n                {\n                    bool hasMigratedVersion = gameMode.Maps\n                        .Where(m => m.UntranslatedName == mapName)\n                        .Any(m => favoriteMaps.Contains($\"{m.SHA1}:{gameModeName}\"));\n\n                    if (hasMigratedVersion)\n                        entriesToRemove.Add(favoriteKey);\n                }\n            }\n\n            // Remove the name-based entries\n            if (entriesToRemove.Any())\n            {\n                foreach (string entry in entriesToRemove)\n                    favoriteMaps.Remove(entry);\n\n                UserINISettings.Instance.WriteFavoriteMaps();\n            }\n        }\n\n        public void PrefetchCachedPreviewImageFromMap(Map map)\n        {\n            if (map?.IsNonImmediatePreviewImageAvailable() ?? false)\n                _ = mapPreviewCacheManager.Request(map, out Image _, addToQueue: true);\n        }\n\n        public Image GetCachedPreviewImageFromMap(Map map, bool syncLoadOnCacheMiss = false)\n        {\n            if (map?.IsImmediatePreviewImageAvailable() ?? false)\n            {\n                return map.GetImmediatePreviewImage();\n            }\n            else if (map?.IsNonImmediatePreviewImageAvailable() ?? false)\n            {\n                if (mapPreviewCacheManager.Request(map, out Image image, syncComputeOnCacheMiss: syncLoadOnCacheMiss, addToQueue: true))\n                    return image;\n                else\n                    return null;\n            }\n            else\n            {\n                return null;\n            }\n        }\n\n        public Map FindMapByHash(string mapHash) => GameModeMaps?.FindMapByHash(mapHash);\n\n        public void Dispose() => mapPreviewCacheManager?.Dispose();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapPreviewCacheManager.cs",
    "content": "#nullable enable\nusing SixLabors.ImageSharp;\n\nnamespace DTAClient.Domain.Multiplayer;\n\n/// <summary>\n/// Thread-safe manager for caching map preview images with LRU eviction policy.\n/// Processes image extraction requests sequentially to limit CPU usage to a single thread.\n/// Note: this manager assumes the `Image` objects are managed, so it never disposes them directly.\n/// </summary>\npublic class MapPreviewCacheManager : CacheManagerBase<Map, Image>, IMapPreviewCacheManager\n{\n    public MapPreviewCacheManager(int capacity) : base(capacity) { }\n\n    public override string Name => nameof(MapPreviewCacheManager);\n\n    protected override Image? ComputeOutputForInput(Map map)\n    {\n        if (!map.IsNonImmediatePreviewImageAvailable())\n            return null;\n\n        Image? image = map.GetNonImmediatePreviewImage();\n        return image;\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs",
    "content": "﻿using System;\nusing System.Buffers;\nusing System.Buffers.Binary;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Text;\nusing ClientCore;\nusing ClientCore.Extensions;\nusing Rampastring.Tools;\nusing lzo.net;\nusing SixLabors.ImageSharp;\nusing SixLabors.ImageSharp.Advanced;\nusing SixLabors.ImageSharp.Memory;\nusing SixLabors.ImageSharp.PixelFormats;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A helper class for extracting preview images from maps.\n    /// </summary>\n    public static class MapPreviewExtractor\n    {\n        /// <summary>\n        /// Extracts map preview image as a bitmap.\n        /// </summary>\n        /// <param name=\"mapIni\">Map file.</param>\n        /// <returns>Bitmap of map preview image, or null if preview could not be extracted.</returns>\n        public static Image ExtractMapPreview(IniFile mapIni)\n        {\n            List<string> sectionKeys = mapIni.GetSectionKeys(\"PreviewPack\");\n\n            string baseFilename = mapIni.FileName.Replace(ProgramConstants.GamePath, \"\");\n\n            if (sectionKeys == null || sectionKeys.Count == 0)\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - no [PreviewPack] exists, unable to extract preview.\");\n                return null;\n            }\n\n            if (mapIni.GetStringValue(\"PreviewPack\", \"1\", string.Empty) ==\n                \"yAsAIAXQ5PDQ5PDQ6JQATAEE6PDQ4PDI4JgBTAFEAkgAJyAATAG0AydEAEABpAJIA0wBVA\")\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - Hidden preview detected, not extracting preview.\");\n                return null;\n            }\n\n            string[] previewSizes = mapIni.GetStringListValue(\"Preview\", \"Size\", string.Empty);\n            int previewWidth = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[2], -1) : -1;\n            int previewHeight = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[3], -1) : -1;\n\n            if (previewWidth < 1 || previewHeight < 1)\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - [Preview] Size value is invalid, unable to extract preview.\");\n                return null;\n            }\n\n            StringBuilder sb = new StringBuilder();\n            if (sectionKeys != null)\n            {\n                foreach (string key in sectionKeys)\n                    sb.Append(mapIni.GetStringValue(\"PreviewPack\", key, string.Empty));\n            }\n\n            byte[] dataSource;\n\n            try\n            {\n                dataSource = Convert.FromBase64String(sb.ToString());\n            }\n            catch (Exception)\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - [PreviewPack] is malformed, unable to extract preview.\");\n                return null;\n            }\n\n            byte[] dataDest = DecompressPreviewData(dataSource, previewWidth * previewHeight * 3, out string errorMessage);\n\n            if (errorMessage != null)\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - \" + errorMessage);\n                return null;\n            }\n\n            Image bitmap = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest, out errorMessage);\n\n            if (errorMessage != null)\n            {\n                Logger.Log(\"MapPreviewExtractor: \" + baseFilename + \" - \" + errorMessage);\n                return null;\n            }\n\n            return bitmap;\n        }\n\n        /// <summary>\n        /// Decompresses map preview image data.\n        /// </summary>\n        /// <param name=\"dataSource\">Array of compressed map preview image data.</param>\n        /// <param name=\"decompressedDataSize\">Size of decompressed preview image data.</param>\n        /// <param name=\"errorMessage\">Will be set to error message if something went wrong, otherwise null.</param>\n        /// <returns>Array of decompressed preview image data if successfully decompressed, otherwise null.</returns>\n        private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedDataSize, out string errorMessage)\n        {\n            try\n            {\n                byte[] dataDest = new byte[decompressedDataSize];\n                int readBytes = 0, writtenBytes = 0;\n\n                while (true)\n                {\n                    if (readBytes >= dataSource.Length)\n                        break;\n\n                    ushort sizeCompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes));\n                    readBytes += 2;\n                    ushort sizeUncompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes));\n                    readBytes += 2;\n\n                    if (sizeCompressed == 0 || sizeUncompressed == 0)\n                        break;\n\n                    if (readBytes + sizeCompressed > dataSource.Length ||\n                        writtenBytes + sizeUncompressed > dataDest.Length)\n                    {\n                        errorMessage = \"Preview data does not match preview size or the data is corrupted, unable to extract preview.\";\n                        return null;\n                    }\n\n                    LzoStream stream = new LzoStream(new MemoryStream(dataSource, readBytes, sizeCompressed), CompressionMode.Decompress);\n                    stream.Read(dataDest, writtenBytes, sizeUncompressed);\n                    readBytes += sizeCompressed;\n                    writtenBytes += sizeUncompressed;\n                }\n\n                errorMessage = null;\n                return dataDest;\n            }\n            catch (Exception ex)\n            {\n                errorMessage = \"Error encountered decompressing preview data. Message: \" + ex.Message;\n                return null;\n            }\n        }\n\n        /// <summary>\n        /// Creates a preview bitmap based on a provided dimensions and raw image pixel data in 24-bit RGB format.\n        /// </summary>\n        /// <param name=\"width\">Width of the bitmap.</param>\n        /// <param name=\"height\">Height of the bitmap.</param>\n        /// <param name=\"imageData\">Raw image pixel data in 24-bit RGB format.</param>\n        /// <param name=\"errorMessage\">Will be set to error message if something went wrong, otherwise null.</param>\n        /// <returns>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.</returns>\n        private static Image CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData, out string errorMessage)\n        {\n            const int pixelFormatBitCount = 24;\n            const int pixelFormatByteCount = pixelFormatBitCount / 8;\n\n            if (imageData.Length != width * height * pixelFormatByteCount)\n            {\n                errorMessage = \"Provided preview image dimensions do not match preview image data length.\";\n                return null;\n            }\n\n            try\n            {\n                int strideWidth = (((width * pixelFormatBitCount) + 31) & ~31) >> 3;\n                int numSkipBytes = strideWidth - (width * pixelFormatByteCount);\n                byte[] bitmapPixelData = new byte[strideWidth * height];\n                int writtenBytes = 0;\n                int readBytes = 0;\n\n                for (int h = 0; h < height; h++)\n                {\n                    for (int w = 0; w < width; w++)\n                    {\n                        // GDI+ bitmap raw pixel data is in BGR format, red & blue values need to be flipped around for each pixel.\n                        bitmapPixelData[writtenBytes] = imageData[readBytes + 2];\n                        bitmapPixelData[writtenBytes + 1] = imageData[readBytes + 1];\n                        bitmapPixelData[writtenBytes + 2] = imageData[readBytes];\n                        writtenBytes += pixelFormatByteCount;\n                        readBytes += pixelFormatByteCount;\n                    }\n\n                    // GDI+ bitmap stride / scan width has to be a multiple of 4, so the end of each stride / scanline can contain extra bytes\n                    // in the bitmap raw pixel data that are not present in the image data and should be skipped when copying.\n                    writtenBytes += numSkipBytes;\n                }\n\n                // https://github.com/SixLabors/ImageSharp/blob/main/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs\n                var image = new Image<Bgr24>(width, height);\n                Configuration configuration = image.GetConfiguration();\n                Buffer2D<Bgr24> imageBuffer = image.Frames.RootFrame.PixelBuffer;\n                using IMemoryOwner<Bgr24> workBuffer = Configuration.Default.MemoryAllocator.Allocate<Bgr24>(width);\n\n                unsafe\n                {\n                    fixed (byte* sourcePtrBase = &bitmapPixelData[0])\n                    {\n                        fixed (Bgr24* destPtr = &workBuffer.Memory.Span[0])\n                        {\n                            for (int rowCount = 0; rowCount < height; rowCount++)\n                            {\n                                Span<Bgr24> row = imageBuffer.DangerousGetRowSpan(rowCount);\n                                byte* sourcePtr = sourcePtrBase + (strideWidth * rowCount);\n\n                                Buffer.MemoryCopy(sourcePtr, destPtr, strideWidth, strideWidth);\n                                PixelOperations<Bgr24>.Instance.FromBgr24(configuration, workBuffer.Memory.Span[..width], row);\n                            }\n                        }\n                    }\n                }\n\n                errorMessage = null;\n\n                return image;\n            }\n            catch (Exception ex)\n            {\n                errorMessage = \"Error encountered creating preview bitmap. Message: \" + ex.Message;\n                return null;\n            }\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/MultiplayerColor.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A color for the multiplayer game lobby.\n    /// </summary>\n    public class MultiplayerColor\n    {\n        public int GameColorIndex { get; private set; }\n        public string Name { get; private set; }\n        public Color XnaColor { get; private set; }\n\n        private static List<MultiplayerColor> colorList;\n\n        /// <summary>\n        /// Creates a new multiplayer color from data in a string array.\n        /// </summary>\n        /// <param name=\"name\">The name of the color.</param>\n        /// <param name=\"data\">The input data. Needs to be in the format R,G,B,(game color index).</param>\n        /// <returns>A new multiplayer color created from the given string array.</returns>\n        public static MultiplayerColor CreateFromStringArray(string name, string[] data)\n        {\n            return new MultiplayerColor()\n            {\n                Name = name,\n                XnaColor = new Color(Math.Min(255, Int32.Parse(data[0])),\n                Math.Min(255, Int32.Parse(data[1])),\n                Math.Min(255, Int32.Parse(data[2])), 255),\n                GameColorIndex = Int32.Parse(data[3])\n            };\n        }\n\n        /// <summary>\n        /// Returns the available multiplayer colors.\n        /// </summary>\n        public static List<MultiplayerColor> LoadColors()\n        {\n            if (colorList != null)\n                return new List<MultiplayerColor>(colorList);\n\n            IniFile gameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"GameOptions.ini\"));\n\n            List<MultiplayerColor> mpColors = new List<MultiplayerColor>();\n\n            List<string> colorKeys = gameOptionsIni.GetSectionKeys(\"MPColors\");\n\n            if (colorKeys == null)\n                throw new ClientConfigurationException(\"[MPColors] not found in GameOptions.ini!\");\n\n            foreach (string key in colorKeys)\n            {\n                string[] values = gameOptionsIni.GetStringListValue(\"MPColors\", key, \"255,255,255,0\");\n\n                try\n                {\n                    MultiplayerColor mpColor = MultiplayerColor.CreateFromStringArray(key.L10N($\"INI:Colors:{key}\"), values);\n                    mpColors.Add(mpColor);\n                }\n                catch\n                {\n                    throw new ClientConfigurationException(\"Invalid MPColor specified in GameOptions.ini: \" + key);\n                }\n            }\n\n            colorList = mpColors;\n            return new List<MultiplayerColor>(colorList);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs",
    "content": "﻿using ClientCore.Extensions;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class PlayerExtraOptions\n    {\n        private static string INVALID_OPTIONS_MESSAGE => \"Invalid player extra options message\".L10N(\"Client:Main:InvalidPlayerExtraOptionsMessage\");\n        private static string MAPPING_ERROR_PREFIX => \"Auto Allying:\".L10N(\"Client:Main:AutoAllyingPrefix\");\n        protected static string NOT_ALL_MAPPINGS_ASSIGNED => MAPPING_ERROR_PREFIX + \" \" + \"You must have all mappings assigned.\".L10N(\"Client:Main:NotAllMappingsAssigned\");\n        protected static string MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START => MAPPING_ERROR_PREFIX + \" \" + \"Multiple mappings assigned to the same start location.\".L10N(\"Client:Main:MultipleMappingsAssigned\");\n        protected static string ONLY_ONE_TEAM => MAPPING_ERROR_PREFIX + \" \" + \"You must have more than one team assigned.\".L10N(\"Client:Main:OnlyOneTeam\");\n        private const char MESSAGE_SEPARATOR = ';';\n\n        public const string CNCNET_MESSAGE_KEY = \"PEO\";\n        public const string LAN_MESSAGE_KEY = \"PEOPTS\";\n\n        public bool IsForceRandomSides { get; set; }\n        public bool IsForceRandomColors { get; set; }\n        public bool IsForceNoTeams { get; set; }\n        public bool IsForceRandomStarts { get; set; }\n        public bool IsUseTeamStartMappings { get; set; }\n        public List<TeamStartMapping> TeamStartMappings { get; set; } = new List<TeamStartMapping>();\n\n        public string GetTeamMappingsError()\n        {\n            if (!IsUseTeamStartMappings)\n                return null;\n\n            var distinctStartLocations = TeamStartMappings.Select(m => m.Start).Distinct();\n            if (distinctStartLocations.Count() != TeamStartMappings.Count)\n                return MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START; // multiple mappings are using the same spawn location\n\n            var distinctTeams = TeamStartMappings.Select(m => m.Team).Distinct();\n            if (distinctTeams.Count() < 2)\n                return ONLY_ONE_TEAM; // must have more than one team assigned\n\n            return null;\n        }\n\n        public string ToCncnetMessage() => $\"{CNCNET_MESSAGE_KEY} {ToString()}\";\n\n        public string ToLanMessage() => $\"{LAN_MESSAGE_KEY} {ToString()}\";\n\n        public override string ToString()\n        {\n            var stringBuilder = new StringBuilder();\n            stringBuilder.Append(IsForceRandomSides ? \"1\" : \"0\");\n            stringBuilder.Append(IsForceRandomColors ? \"1\" : \"0\");\n            stringBuilder.Append(IsForceNoTeams ? \"1\" : \"0\");\n            stringBuilder.Append(IsForceRandomStarts ? \"1\" : \"0\");\n            stringBuilder.Append(IsUseTeamStartMappings ? \"1\" : \"0\");\n            stringBuilder.Append(MESSAGE_SEPARATOR);\n            stringBuilder.Append(TeamStartMapping.ToListString(TeamStartMappings));\n\n            return stringBuilder.ToString();\n        }\n\n        public static PlayerExtraOptions FromMessage(string message)\n        {\n            var parts = message.Split(MESSAGE_SEPARATOR);\n            if (parts.Length < 2)\n                throw new Exception(INVALID_OPTIONS_MESSAGE);\n\n            var boolParts = parts[0].ToCharArray();\n            if (boolParts.Length < 5)\n                throw new Exception(INVALID_OPTIONS_MESSAGE);\n\n            return new PlayerExtraOptions\n            {\n                IsForceRandomSides = boolParts[0] == '1',\n                IsForceRandomColors = boolParts[1] == '1',\n                IsForceNoTeams = boolParts[2] == '1',\n                IsForceRandomStarts = boolParts[3] == '1',\n                IsUseTeamStartMappings = boolParts[4] == '1',\n                TeamStartMappings = TeamStartMapping.FromListString(parts[1])\n            };\n        }\n\n        public bool IsDefault()\n        {\n            var defaultPLayerExtraOptions = new PlayerExtraOptions();\n            return IsForceRandomColors == defaultPLayerExtraOptions.IsForceRandomColors &&\n                   IsForceRandomStarts == defaultPLayerExtraOptions.IsForceRandomStarts &&\n                   IsForceNoTeams == defaultPLayerExtraOptions.IsForceNoTeams &&\n                   IsForceRandomSides == defaultPLayerExtraOptions.IsForceRandomSides &&\n                   IsUseTeamStartMappings == defaultPLayerExtraOptions.IsUseTeamStartMappings;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing ClientCore;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class PlayerHouseInfo\n    {\n        public int SideIndex { get; set; }\n\n        /// <summary>\n        /// A side (or, more correctly, house or country depending on the game)\n        /// index that is used in rules file of the game.\n        /// </summary>\n        public int InternalSideIndex\n        {\n            get\n            {\n                if (IsSpectator && !string.IsNullOrEmpty(ClientConfiguration.Instance.SpectatorInternalSideIndex))\n                    return int.Parse(ClientConfiguration.Instance.SpectatorInternalSideIndex);\n                \n                if (!string.IsNullOrEmpty(ClientConfiguration.Instance.InternalSideIndices))\n                    return Array.ConvertAll(ClientConfiguration.Instance.InternalSideIndices.Split(','), int.Parse)[SideIndex];\n\n                return SideIndex;\n            }\n        }\n        public int ColorIndex { get; set; }\n        public int StartingWaypoint { get; set; }\n\n        public int RealStartingWaypoint { get; set; }\n\n        public bool IsSpectator { get; set; }\n\n        /// <summary>\n        /// Applies the player's side into the information\n        /// and randomizes it if necessary.\n        /// </summary>\n        /// <param name=\"pInfo\">The PlayerInfo of the player.</param>\n        /// <param name=\"sideCount\">The number of sides in the game.</param>\n        /// <param name=\"random\">Random number generator.</param>\n        /// <param name=\"disallowedSideArray\">A bool array that determines which side indexes are disallowed by game options.</param>\n        public void RandomizeSide(PlayerInfo pInfo, int sideCount, Random random,\n            bool[] disallowedSideArray, List<int[]> randomSelectors, int randomCount)\n        {\n            if (pInfo.SideId == 0 || pInfo.SideId == sideCount + randomCount)\n            {\n                // The player has selected Random or Spectator\n\n                int sideId;\n\n                do sideId = random.Next(0, sideCount);\n                while (disallowedSideArray[sideId]);\n\n                SideIndex = sideId;\n            }\n            else\n            {\n                // Use custom random selector.\n                if (pInfo.SideId < randomCount)\n                {\n                    int[] randomsides = randomSelectors[pInfo.SideId - 1];\n                    int count = randomsides.Length;\n                    int sideId;\n                    \n                    do sideId = randomsides[random.Next(0, count)];\n                    while (disallowedSideArray[sideId]);\n\n                    SideIndex = sideId;\n                }\n                else SideIndex = pInfo.SideId - randomCount; // The player has selected a side\n            }\n        }\n\n        /// <summary>\n        /// Applies the player's color into the information and randomizes\n        /// it if necessary. If the color is randomized, it's removed\n        /// from the list of available colors.\n        /// </summary>\n        /// <param name=\"pInfo\">The PlayerInfo of the player.</param>\n        /// <param name=\"freeColors\">The list of available (un-used) colors.</param>\n        /// <param name=\"mpColors\">The list of all multiplayer colors.</param>\n        /// <param name=\"random\">Random number generator.</param>\n        public void RandomizeColor(PlayerInfo pInfo, List<int> freeColors, \n            List<MultiplayerColor> mpColors, Random random)\n        {\n            if (pInfo.ColorId == 0)\n            {\n                // The player has selected Random for their color\n\n                int randomizedColorIndex = random.Next(0, freeColors.Count);\n                int actualColorId = freeColors[randomizedColorIndex];\n\n                ColorIndex = mpColors[actualColorId].GameColorIndex;\n                freeColors.RemoveAt(randomizedColorIndex);\n            }\n            else\n            {\n                ColorIndex = mpColors[pInfo.ColorId - 1].GameColorIndex;\n                freeColors.Remove(pInfo.ColorId - 1);\n            }\n        }\n\n        /// <summary>\n        /// Applies the player's starting location into the information and\n        /// randomizes it if necessary. If the starting location is randomized,\n        /// the starting location is removed from the list of available starting locations.\n        /// </summary>\n        /// <param name=\"pInfo\">The PlayerInfo of the player.</param>\n        /// <param name=\"freeStartingLocations\">List of free starting locations.</param>\n        /// <param name=\"random\">Random number generator.</param>\n        /// <param name=\"takenStartingLocations\">A list of starting locations that are already occupied.</param>\n        /// <param name=\"overrideGameRandomLocations\"></param>\n        /// <returns>True if the player's starting location index exceeds the map's number of starting waypoints,\n        /// otherwise false.</returns>\n        public void RandomizeStart(\n            PlayerInfo pInfo, \n            Random random,\n            List<int> freeStartingLocations, \n            List<int> takenStartingLocations,\n            bool overrideGameRandomLocations\n        )\n        {\n            overrideGameRandomLocations |= ClientConfiguration.Instance.UseClientRandomStartLocations;\n            if (IsSpectator)\n            {\n                StartingWaypoint = 90;\n                return;\n            }\n\n            if (pInfo.StartingLocation == 0)\n            {\n                // Randomize starting location\n\n                if (!overrideGameRandomLocations)\n                {\n                    // The game uses its own randomization logic that places\n                    // randomized players on the opposite side of the map\n                    // Players seem to prefer this behaviour, so use -1 to\n                    // leave randomizing the starting location to the game itself\n                    RealStartingWaypoint = -1;\n                    StartingWaypoint = -1;\n                    return;\n                }\n\n                // Let the client pick starting positions.\n                if (freeStartingLocations.Count == 0) // No free starting locs available\n                {\n                    RealStartingWaypoint = -1;\n                    StartingWaypoint = -1;\n                    return;\n                }\n\n                int waypointIndex = random.Next(0, freeStartingLocations.Count);\n                RealStartingWaypoint = freeStartingLocations[waypointIndex];\n                StartingWaypoint = RealStartingWaypoint;\n                freeStartingLocations.Remove(StartingWaypoint);\n                return;\n            }\n\n            // Use the player's selected starting location\n            RealStartingWaypoint = pInfo.StartingLocation - 1;\n\n            if (takenStartingLocations.Contains(RealStartingWaypoint))\n            {\n                StartingWaypoint = -1; // Unknown starting location, stacked with another player\n                return;\n            }\n\n            takenStartingLocations.Add(RealStartingWaypoint);\n\n            StartingWaypoint = RealStartingWaypoint;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/PlayerInfo.cs",
    "content": "﻿using Rampastring.Tools;\nusing System;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    /// <summary>\n    /// A player in the game lobby.\n    /// </summary>\n    public class PlayerInfo\n    {\n        public PlayerInfo() { }\n\n        public PlayerInfo(string name)\n        {\n            Name = name;\n        }\n\n        public PlayerInfo(string name, int sideId, int startingLocation, int colorId, int teamId)\n        {\n            Name = name;\n            SideId = sideId;\n            StartingLocation = startingLocation;\n            ColorId = colorId;\n            TeamId = teamId;\n        }\n\n        public string Name { get; set; }\n        public int SideId { get; set; }\n        public int StartingLocation { get; set; }\n        public int ColorId { get; set; }\n        public int TeamId { get; set; }\n        public bool Ready { get; set; }\n        public bool AutoReady { get; set; }\n        public bool IsAI { get; set; }\n\n        public bool IsInGame { get; set; }\n        public virtual string IPAddress { get; set; } = \"0.0.0.0\";\n        public int Port { get; set; }\n\n        /// <summary>\n        /// Whether the file hash information is received from the player, regardless of whether it is consistent with the one calculated by this client.\n        /// </summary>\n        public bool HashReceived { get; set; }\n\n        public int Index { get; set; }\n\n        public int Ping { get; set; } = -1;\n\n        /// <summary>\n        /// The difficulty level of an AI player for in-client purposes.\n        /// Logical increasing scale, like in the vanilla Tiberian Sun UI.\n        /// 2 = Hard, 1 = Medium, 0 = Easy.\n        /// </summary>\n        public int AILevel { get; set; }\n\n        /// <summary>\n        /// The AI level of the AI for the [HouseHandicaps] section in spawn.ini.\n        /// 2 = Easy, 1 = Medium, 0 = Hard.\n        /// </summary>\n        public int HouseHandicapAILevel\n        {\n            get { return Math.Abs(AILevel - 2); }\n        }\n\n        public override string ToString()\n        {\n            var sb = new ExtendedStringBuilder(true, ',');\n            sb.Append(Name);\n            sb.Append(SideId);\n            sb.Append(StartingLocation);\n            sb.Append(ColorId);\n            sb.Append(TeamId);\n            sb.Append(AILevel);\n            sb.Append(IsAI.ToString());\n            sb.Append(Index);\n            return sb.ToString();\n        }\n\n        /// <summary>\n        /// Creates a PlayerInfo instance from a string in a format that matches the \n        /// string given by the ToString() method.\n        /// </summary>\n        /// <param name=\"str\">The string.</param>\n        /// <returns>A PlayerInfo instance, or null if the string format was invalid.</returns>\n        public static PlayerInfo FromString(string str)\n        {\n            var values = str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);\n\n            if (values.Length != 8)\n                return null;\n\n            var pInfo = new PlayerInfo();\n\n            pInfo.Name = values[0];\n            pInfo.SideId = Conversions.IntFromString(values[1], 0);\n            pInfo.StartingLocation = Conversions.IntFromString(values[2], 0);\n            pInfo.ColorId = Conversions.IntFromString(values[3], 0);\n            pInfo.TeamId = Conversions.IntFromString(values[4], 0);\n            pInfo.AILevel = Conversions.IntFromString(values[5], 0);\n            pInfo.IsAI = Conversions.BooleanFromString(values[6], true);\n            pInfo.Index = Conversions.IntFromString(values[7], 0);\n\n            return pInfo;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/SavedGamePlayer.cs",
    "content": "﻿namespace DTAClient.Domain.Multiplayer\n{\n    public class SavedGamePlayer\n    {\n        public string Name { get; set; }\n        public int ColorIndex { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/TeamStartMapping.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\nusing ClientCore;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class TeamStartMapping\n    {\n        private const char LIST_SEPARATOR = ',';\n\n        public const string NO_PLAYER = \"x\";\n        public const string NO_TEAM = \"-\";\n        public static readonly List<string> TEAMS = new List<string>() { NO_PLAYER, NO_TEAM }.Concat(ProgramConstants.TEAMS).ToList();\n\n        [JsonInclude]\n        [JsonPropertyName(\"t\")]\n        public string Team { get; set; }\n\n        [JsonInclude]\n        [JsonPropertyName(\"s\")]\n        public int Start { get; set; }\n\n        [JsonIgnore]\n        public bool IsValid => TeamIndex != -1;\n\n        [JsonIgnore]\n        public int TeamIndex => TEAMS.IndexOf(Team);\n\n        [JsonIgnore]\n        public int TeamId => ProgramConstants.TEAMS.IndexOf(Team) + 1;\n\n        [JsonIgnore]\n        public int StartingWaypoint => Start - 1;\n\n        [JsonIgnore]\n        public bool IsBlock => Team == NO_PLAYER;\n\n        /// <summary>\n        /// Write these out in a delimited list.\n        /// </summary>\n        /// <param name=\"teamStartMappings\"></param>\n        /// <returns></returns>\n        public static string ToListString(List<TeamStartMapping> teamStartMappings)\n            => string.Join(LIST_SEPARATOR.ToString(), teamStartMappings.Select(mapping => mapping.Team));\n\n        /// <summary>\n        /// This parses a list of <see cref=\"TeamStartMapping\"/> classes that were written out as a list\n        /// for either message purposes or a map INI.\n        /// </summary>\n        /// <param name=\"str\"></param>\n        /// <returns></returns>\n        public static List<TeamStartMapping> FromListString(string str)\n        {\n            if (string.IsNullOrWhiteSpace(str))\n                return new List<TeamStartMapping>();\n\n            var parts = str.Split(LIST_SEPARATOR);\n\n            return parts.Select((part, index) => new TeamStartMapping()\n            {\n                Team = part,\n                Start = index + 1\n            }).ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/Multiplayer/TeamStartMappingPreset.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace DTAClient.Domain.Multiplayer\n{\n    public class TeamStartMappingPreset\n    {\n        [JsonInclude]\n        [JsonPropertyName(\"n\")]\n        public string Name { get; set; }\n\n        [JsonInclude]\n        [JsonPropertyName(\"m\")]\n        public List<TeamStartMapping> TeamStartMappings { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Domain/SavedGame.cs",
    "content": "﻿using ClientCore;\nusing Rampastring.Tools;\nusing System;\nusing System.Buffers.Binary;\nusing System.IO;\nusing OpenMcdf;\nusing System.Diagnostics;\n\nnamespace DTAClient.Domain\n{\n    /// <summary>\n    /// A single-player saved game.\n    /// </summary>\n    public class SavedGame\n    {\n        const string SAVED_GAME_PATH = \"Saved Games/\";\n\n        public SavedGame(string fileName)\n        {\n            FileName = fileName;\n        }\n\n        public string FileName { get; private set; }\n        public string GUIName { get; private set; }\n        public DateTime LastModified { get; private set; }\n        public int CustomMissionID { get; private set; }\n\n        /// <summary>\n        /// Reads and sets the saved game's name and last modified date, and returns true if succesful.\n        /// </summary>\n        /// <returns>True if parsing the info was succesful, otherwise false.</returns>\n        public bool ParseInfo()\n        {\n            try\n            {\n                FileInfo savedGameFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SAVED_GAME_PATH, FileName);\n\n                using (Stream file = savedGameFileInfo.Open(FileMode.Open, FileAccess.Read))\n                {\n                    var cf = new CompoundFile(file);\n\n                    GUIName = System.Text.Encoding.Unicode.GetString(cf.RootStorage.GetStream(\"Scenario Description\").GetData()).TrimEnd(['\\0']);\n                    try\n                    {\n                        CustomMissionID = BinaryPrimitives.ReadInt32LittleEndian(cf.RootStorage.GetStream(\"CustomMissionID\").GetData());\n                    }\n                    catch (CFItemNotFound)\n                    {\n                        CustomMissionID = 0;\n                    }\n                }\n\n                LastModified = savedGameFileInfo.LastWriteTime;\n                return true;\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"An error occured while parsing saved game \" + FileName + \":\" +\n                    ex.ToString());\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/Channel.cs",
    "content": "﻿using ClientCore;\nusing ClientCore.Enums;\nusing DTAClient.Online.EventArguments;\nusing System;\nusing System.Collections.Generic;\nusing DTAClient.DXGUI;\nusing ClientCore.Extensions;\nusing System.Diagnostics;\n\nnamespace DTAClient.Online\n{\n    public class Channel : IMessageView\n    {\n        const int MESSAGE_LIMIT = 1024;\n\n        public event EventHandler<ChannelUserEventArgs> UserAdded;\n        public event EventHandler<UserNameEventArgs> UserLeft;\n        public event EventHandler<UserNameEventArgs> UserKicked;\n        public event EventHandler<UserNameEventArgs> UserQuitIRC;\n        public event EventHandler<ChannelUserEventArgs> UserGameIndexUpdated;\n        public event EventHandler<UserNameChangedEventArgs> UserNameChanged;\n        public event EventHandler UserListReceived;\n        public event EventHandler UserListCleared;\n\n        public event EventHandler<IRCMessageEventArgs> MessageAdded;\n        public event EventHandler<ChannelModeEventArgs> ChannelModesChanged;\n        public event EventHandler<ChannelCTCPEventArgs> CTCPReceived;\n        public event EventHandler InvalidPasswordEntered;\n        public event EventHandler InviteOnlyErrorOnJoin;\n\n        /// <summary>\n        /// Raised when the server informs the client that it's is unable to\n        /// join the channel because it's full.\n        /// </summary>\n        public event EventHandler ChannelFull;\n\n        /// <summary>\n        /// Raised when the server informs the client that it's is unable to\n        /// join the channel because the client has attempted to join too many\n        /// channels too quickly.\n        /// </summary>\n        public event EventHandler<MessageEventArgs> TargetChangeTooFast;\n\n        public Channel(string uiName, string channelName, bool persistent, bool isChatChannel, string password, Connection connection)\n        {\n            if (isChatChannel)\n                users = new SortedUserCollection<ChannelUser>(ChannelUser.ChannelUserComparison);\n            else\n                users = new UnsortedUserCollection<ChannelUser>();\n\n            UIName = uiName;\n            ChannelName = channelName.ToLowerInvariant();\n            Persistent = persistent;\n            IsChatChannel = isChatChannel;\n            Password = password;\n            this.connection = connection;\n\n            if (persistent)\n            {\n                Instance_SettingsSaved(null, EventArgs.Empty);\n                UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved;\n            }\n        }\n\n        #region Public members\n\n        public string UIName { get; set; }\n\n        public string ChannelName { get; }\n\n        public bool Persistent { get; }\n\n        public bool IsChatChannel { get; }\n\n        public string Password { get; private set; }\n\n        private readonly Connection connection;\n\n        string _topic;\n        public string Topic\n        {\n            get { return _topic; }\n            set\n            {\n                _topic = value;\n                if (Persistent)\n                    AddMessage(new ChatMessage(\n                        string.Format(\"Topic for {0} is: {1}\".L10N(\"Client:Main:ChannelTopic\"), UIName, _topic)));\n            }\n        }\n\n        List<ChatMessage> messages = new List<ChatMessage>();\n        public List<ChatMessage> Messages => messages;\n\n        IUserCollection<ChannelUser> users;\n        public IUserCollection<ChannelUser> Users => users;\n\n        #endregion\n\n        bool notifyOnUserListChange = true;\n\n        private void Instance_SettingsSaved(object sender, EventArgs e)\n        {\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.YR)\n                notifyOnUserListChange = false;\n            else\n                notifyOnUserListChange = UserINISettings.Instance.NotifyOnUserListChange;\n        }\n\n        public void AddUser(ChannelUser user)\n        {\n            users.Add(user.IRCUser.Name, user);\n            UserAdded?.Invoke(this, new ChannelUserEventArgs(user));\n        }\n\n        public void OnUserJoined(ChannelUser user)\n        {\n            AddUser(user);\n\n            if (notifyOnUserListChange)\n            {\n                AddMessage(new ChatMessage(\n                    string.Format(\"{0} has joined {1}.\".L10N(\"Client:Main:PlayerJoinChannel\"), user.IRCUser.Name, UIName)));\n            }\n\n            if (ClientConfiguration.Instance.ClientGameType != ClientType.YR)\n            {\n                if (Persistent && IsChatChannel && user.IRCUser.Name == ProgramConstants.PLAYERNAME)\n                    RequestUserInfo();\n            }\n        }\n\n        public void OnUserListReceived(List<ChannelUser> userList)\n        {\n            for (int i = 0; i < userList.Count; i++)\n            {\n                ChannelUser user = userList[i];\n                var existingUser = users.Find(user.IRCUser.Name);\n                if (existingUser == null)\n                {\n                    users.Add(user.IRCUser.Name, user);\n                }\n                else if (IsChatChannel)\n                {\n                    if (existingUser.IsAdmin != user.IsAdmin)\n                    {\n                        existingUser.IsAdmin = user.IsAdmin;\n                        existingUser.IsFriend = user.IsFriend;\n\n                        // Note: IUserCollection.Reinsert() is not guaranteed to be implemented, unless it is a SortedUserCollection\n                        Debug.Assert(users is SortedUserCollection<ChannelUser>, \"Channel 'users' is supposed to be a SortedUserCollection\");\n                        users.Reinsert(user.IRCUser.Name);\n                    }\n                }\n            }\n\n            UserListReceived?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void OnUserKicked(string userName)\n        {\n            if (users.Remove(userName))\n            {\n                if (userName == ProgramConstants.PLAYERNAME)\n                {\n                    users.Clear();\n                }\n\n                AddMessage(new ChatMessage(\n                    string.Format(\"{0} has been kicked from {1}.\".L10N(\"Client:Main:PlayerKickedFromChannel\"), userName, UIName)));\n\n                UserKicked?.Invoke(this, new UserNameEventArgs(userName));\n            }\n        }\n\n        public void OnUserLeft(string userName)\n        {\n            if (users.Remove(userName))\n            {\n                if (notifyOnUserListChange)\n                {\n                    AddMessage(new ChatMessage(\n                         string.Format(\"{0} has left from {1}.\".L10N(\"Client:Main:PlayerLeftFromChannel\"), userName, UIName)));\n                }\n\n                UserLeft?.Invoke(this, new UserNameEventArgs(userName));\n            }\n        }\n\n        public void OnUserQuitIRC(string userName)\n        {\n            if (users.Remove(userName))\n            {\n                if (notifyOnUserListChange)\n                {\n                    AddMessage(new ChatMessage(\n                        string.Format(\"{0} has quit from CnCNet.\".L10N(\"Client:Main:PlayerQuitCncNet\"), userName)));\n                }\n\n                UserQuitIRC?.Invoke(this, new UserNameEventArgs(userName));\n            }\n        }\n\n        public void UpdateGameIndexForUser(string userName)\n        {\n            var user = users.Find(userName);\n            if (user != null)\n                UserGameIndexUpdated?.Invoke(this, new ChannelUserEventArgs(user));\n        }\n\n        public void OnUserNameChanged(string oldUserName, string newUserName)\n        {\n            var user = users.Find(oldUserName);\n            if (user != null)\n            {\n                users.Remove(oldUserName);\n                users.Add(newUserName, user);\n                UserNameChanged?.Invoke(this, new UserNameChangedEventArgs(oldUserName, user.IRCUser));\n            }\n        }\n\n        public void OnChannelModesChanged(string sender, string modes)\n        {\n            ChannelModesChanged?.Invoke(this, new ChannelModeEventArgs(sender, modes));\n        }\n\n        public void OnCTCPReceived(string userName, string message)\n        {\n            CTCPReceived?.Invoke(this, new ChannelCTCPEventArgs(userName, message));\n        }\n\n        public void OnInvalidJoinPassword()\n        {\n            InvalidPasswordEntered?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void OnInviteOnlyOnJoin()\n        {\n            InviteOnlyErrorOnJoin?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void OnChannelFull()\n        {\n            ChannelFull?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void OnTargetChangeTooFast(string message)\n        {\n            TargetChangeTooFast?.Invoke(this, new MessageEventArgs(message));\n        }\n\n        public void AddMessage(ChatMessage message)\n        {\n            if (messages.Count == MESSAGE_LIMIT)\n                messages.RemoveAt(0);\n\n            messages.Add(message);\n\n            MessageAdded?.Invoke(this, new IRCMessageEventArgs(message));\n        }\n\n        public void SendChatMessage(string message, IRCColor color)\n        {\n            AddMessage(new ChatMessage(ProgramConstants.PLAYERNAME, color.XnaColor, DateTime.Now, message));\n\n            string colorString = ((char)03).ToString() + color.IrcColorId.ToString(\"D2\");\n\n            connection.QueueMessage(QueuedMessageType.CHAT_MESSAGE, 0,\n                \"PRIVMSG \" + ChannelName + \" :\" + colorString + message);\n        }\n\n        /// <param name=\"message\"></param>\n        /// <param name=\"qmType\"></param>\n        /// <param name=\"priority\"></param>\n        /// <param name=\"replace\">\n        ///     This can be used to help prevent flooding for multiple options that are changed quickly. It allows for a single message\n        ///     for multiple changes.\n        /// </param>\n        public void SendCTCPMessage(string message, QueuedMessageType qmType, int priority, bool replace = false)\n        {\n            char CTCPChar1 = (char)58;\n            char CTCPChar2 = (char)01;\n\n            connection.QueueMessage(qmType, priority,\n                \"NOTICE \" + ChannelName + \" \" + CTCPChar1 + CTCPChar2 + message + CTCPChar2, replace);\n        }\n\n        /// <summary>\n        /// Sends a \"kick user\" message to the channel.\n        /// </summary>\n        /// <param name=\"userName\">The name of the user that should be kicked.</param>\n        /// <param name=\"priority\">The priority of the message in the send queue.</param>\n        public void SendKickMessage(string userName, int priority)\n        {\n            connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, \"KICK \" + ChannelName + \" \" + userName);\n        }\n\n        /// <summary>\n        /// Sends a \"ban host\" message to the channel.\n        /// </summary>\n        /// <param name=\"host\">The host that should be banned.</param>\n        /// <param name=\"priority\">The priority of the message in the send queue.</param>\n        public void SendBanMessage(string host, int priority)\n        {\n            connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority,\n                string.Format(\"MODE {0} +b *!*@{1}\", ChannelName, host));\n        }\n\n        /// <summary>\n        /// Changes the channel password.\n        /// </summary>\n        /// <param name=\"newPassword\">The new password. If empty, removes the password.</param>\n        /// <param name=\"priority\">The priority of the message in the send queue.</param>\n        public void ChangePassword(string newPassword, int priority)\n        {\n            string oldPassword = Password;\n            Password = newPassword;\n\n            if (string.IsNullOrEmpty(newPassword))\n            {\n                // remove\n                connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority,\n                    string.Format(\"MODE {0} -k {1}\", ChannelName, oldPassword));\n            }\n            else if (string.IsNullOrEmpty(oldPassword))\n            {\n                // add\n                connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority,\n                    string.Format(\"MODE {0} +k {1}\", ChannelName, newPassword));\n            }\n            else\n            {\n                // update (remove + add - both passwords need to be known)\n                connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority,\n                    string.Format(\"MODE {0} -k+k {1} {2}\", ChannelName, oldPassword, newPassword));\n            }\n        }\n\n        public void Join()\n        {\n            // Wait a random amount of time before joining to prevent join/part floods\n            if (Persistent)\n            {\n                int rn = connection.Rng.Next(1, 10000);\n\n                if (string.IsNullOrEmpty(Password))\n                    connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, \"JOIN \" + ChannelName);\n                else\n                    connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, \"JOIN \" + ChannelName + \" \" + Password);\n            }\n            else\n            {\n                if (string.IsNullOrEmpty(Password))\n                    connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, \"JOIN \" + ChannelName);\n                else\n                    connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, \"JOIN \" + ChannelName + \" \" + Password);\n            }\n        }\n\n        public void RequestUserInfo()\n        {\n            connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, \"WHO \" + ChannelName);\n        }\n\n        public void Leave()\n        {\n            // Wait a random amount of time before joining to prevent join/part floods\n            if (Persistent)\n            {\n                int rn = connection.Rng.Next(1, 10000);\n                connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, \"PART \" + ChannelName);\n            }\n            else\n            {\n                connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, \"PART \" + ChannelName);\n            }\n            ClearUsers();\n        }\n\n        public void ClearUsers()\n        {\n            users.Clear();\n            UserListCleared?.Invoke(this, EventArgs.Empty);\n        }\n    }\n\n    public class ChannelUserEventArgs : EventArgs\n    {\n        public ChannelUserEventArgs(ChannelUser user)\n        {\n            User = user;\n        }\n\n        public ChannelUser User { get; private set; }\n    }\n\n    public class UserNameIndexEventArgs : EventArgs\n    {\n        public UserNameIndexEventArgs(int index, string userName)\n        {\n            UserIndex = index;\n            UserName = userName;\n        }\n\n        public int UserIndex { get; private set; }\n        public string UserName { get; private set; }\n    }\n\n    public class UserNameEventArgs : EventArgs\n    {\n        public UserNameEventArgs(string userName)\n        {\n            UserName = userName;\n        }\n\n        public string UserName { get; private set; }\n    }\n\n    public class IRCMessageEventArgs : EventArgs\n    {\n        public IRCMessageEventArgs(ChatMessage ircMessage)\n        {\n            Message = ircMessage;\n        }\n\n        public ChatMessage Message { get; private set; }\n    }\n\n    public class MessageEventArgs : EventArgs\n    {\n        public MessageEventArgs(string message)\n        {\n            Message = message;\n        }\n\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/ChannelUser.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// An user on an IRC channel.\n    /// </summary>\n    public class ChannelUser\n    {\n        public ChannelUser(IRCUser ircUser)\n        {\n            IRCUser = ircUser;\n        }\n\n        public IRCUser IRCUser { get; private set; }\n\n        public bool IsAdmin { get; set; }\n\n        public bool IsFriend { get; set; }\n        \n        public bool HasVoice => IRCUser.HasVoice;\n\n        public static int ChannelUserComparison(ChannelUser u1, ChannelUser u2)\n        {\n            if (u1.IsAdmin != u2.IsAdmin)\n                return u1.IsAdmin ? -1 : 1;\n\n            if (u1.HasVoice != u2.HasVoice)\n                return u1.HasVoice ? -1 : 1;\n            \n            if (u1.IsFriend != u2.IsFriend)\n                return u1.IsFriend ? -1 : 1;\n\n            return string.Compare(u1.IRCUser.Name, u2.IRCUser.Name, StringComparison.InvariantCultureIgnoreCase);\n\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Online/ChatMessage.cs",
    "content": "﻿using Microsoft.Xna.Framework;\nusing System;\n\nnamespace DTAClient.Online\n{\n    public class ChatMessage\n    {\n        /// <summary>\n        /// Creates a new ChatMessage instance.\n        /// </summary>\n        /// <param name=\"senderName\">The sender of the message. Use null for none (system messages).</param>\n        /// <param name=\"color\">The color of the message.</param>\n        /// <param name=\"dateTime\">The date and time of the message.</param>\n        /// <param name=\"message\">The message.</param>\n        public ChatMessage(string senderName, Color color, DateTime dateTime, string message)\n        {\n            SenderName = senderName;\n            Color = color;\n            DateTime = dateTime;\n            Message = message;\n        }\n\n        /// <summary>\n        /// Creates a chat message with the date and time set to the current system date and time.\n        /// </summary>\n        /// <param name=\"senderName\">The sender of the message. Use null for none (system messages).</param>\n        /// <param name=\"color\">The color of the message.</param>\n        /// <param name=\"message\">The message.</param>\n        public ChatMessage(string senderName, Color color, string message) : this(senderName, color, DateTime.Now, message) { }\n\n        /// <summary>\n        /// Creates a new ChatMessage instance.\n        /// </summary>\n        /// <param name=\"senderName\">The sender of the message. Use null for none (system messages).</param>\n        /// <param name=\"ident\">The IRC identifier of the sender.</param>\n        /// <param name=\"senderIsAdmin\">The sender of the message is a channel admin.</param>\n        /// <param name=\"color\">The color of the message.</param>\n        /// <param name=\"dateTime\">The date and time of the message.</param>\n        /// <param name=\"message\">The message.</param>\n        public ChatMessage(string senderName, string ident, bool senderIsAdmin, Color color, DateTime dateTime, string message) : this(senderName, color, dateTime, message)\n        {\n            SenderIdent = ident;\n            SenderIsAdmin = senderIsAdmin;\n        }\n\n        /// <summary>\n        /// Creates a chat message that has no sender and has the date and time set to the\n        /// current system date and time.\n        /// </summary>\n        /// <param name=\"color\">The color of the message.</param>\n        /// <param name=\"message\">The message.</param>\n        public ChatMessage(Color color, string message) : this(null, color, DateTime.Now, message) { }\n\n        /// <summary>\n        /// Creates a chat message that has no sender and has the date and time set to the\n        /// current system date and time.\n        /// </summary>\n        /// <param name=\"message\">The message.</param>\n        public ChatMessage(string message) : this(Color.White, message) { }\n\n        public string SenderName { get; private set; }\n        public string SenderIdent { get; private set; }\n        public Color Color { get; private set; }\n        public DateTime DateTime { get; private set; }\n        public string Message { get; private set; }\n        public bool SenderIsAdmin { get; private set; }\n\n        public bool IsUser => SenderIdent != null;\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/CnCNetGameCheck.cs",
    "content": "﻿using ClientCore;\nusing System.Diagnostics;\nusing System.Threading;\n\nnamespace DTAClient.Online\n{\n    public sealed class CnCNetGameCheck\n    {\n        private static readonly CnCNetGameCheck _instance = new CnCNetGameCheck();\n\n        private static readonly int REFRESH_INTERVAL = 15000; // 15 seconds\n\n        private CnCNetGameCheck() { }\n\n        public static CnCNetGameCheck Instance => _instance;\n\n        public void InitializeService(CancellationTokenSource cts)\n        {\n            ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts);\n        }\n\n        private void RunService(object tokenObj)\n        {\n            var waitHandle = ((CancellationTokenSource)tokenObj).Token.WaitHandle;\n\n            while (true)\n            {\n                if (waitHandle.WaitOne(REFRESH_INTERVAL))\n                {\n                    // Cancellation signaled\n                    return;\n                }\n                else\n                {\n                    CheatEngineWatchEvent();\n                }\n            }\n        }\n\n        private void CheatEngineWatchEvent()\n        {\n            if (!ProgramConstants.IsInGame) \n                return;\n\n            Process[] processlist = Process.GetProcesses();\n            foreach (Process process in processlist)\n            {\n                try {\n                    if (process.ProcessName.Contains(\"cheatengine\") ||\n                        process.MainWindowTitle.ToLower().Contains(\"cheat engine\")\n                        )\n                    {\n                        KillGameInstance();\n                    }\n                }\n                catch { }\n\n                process.Dispose();\n            }\n        }\n\n        private void KillGameInstance()\n        {\n            try\n            {\n                string gameExecutableName = ClientConfiguration.Instance.GetOperatingSystemVersion() == OSVersion.UNIX ?\n                    ClientConfiguration.Instance.UnixGameExecutableName :\n                    ClientConfiguration.Instance.GetGameExecutableName();\n\n                gameExecutableName = gameExecutableName.Replace(\".exe\", \"\");\n\n                Process[] processlist = Process.GetProcesses();\n                foreach (Process process in processlist)\n                {\n                    try {\n                        if (process.ProcessName.Contains(gameExecutableName))\n                        {\n                            process.Kill();\n                        }\n                    }\n                    catch { }\n\n                    process.Dispose();\n                }\n            }\n            catch\n            {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/CnCNetManager.cs",
    "content": "﻿using ClientCore;\nusing DTAClient.Domain.Multiplayer.CnCNet;\nusing DTAClient.Online.EventArguments;\nusing ClientCore.Extensions;\nusing Microsoft.Xna.Framework;\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// Acts as an interface between the CnCNet connection class\n    /// and the user-interface's classes.\n    /// </summary>\n    public class CnCNetManager : IConnectionManager\n    {\n        // When implementing IConnectionManager functions, pay special attention\n        // to thread-safety.\n        // The functions in IConnectionManager are usually called from the networking\n        // thread, so if they affect anything in the UI or affect data that the \n        // UI thread might be reading, use WindowManager.AddCallback to execute a function\n        // on the UI thread instead of modifying the data or raising events directly.\n\n        public delegate void UserListDelegate(string channelName, string[] userNames);\n\n        public event EventHandler<ServerMessageEventArgs> WelcomeMessageReceived;\n        public event EventHandler<UserAwayEventArgs> AwayMessageReceived;\n        public event EventHandler<WhoEventArgs> WhoReplyReceived;\n        public event EventHandler<CnCNetPrivateMessageEventArgs> PrivateMessageReceived;\n        public event EventHandler<PrivateCTCPEventArgs> PrivateCTCPReceived;\n        public event EventHandler<ChannelEventArgs> BannedFromChannel;\n\n        public event EventHandler<AttemptedServerEventArgs> AttemptedServerChanged;\n        public event EventHandler ConnectAttemptFailed;\n        public event EventHandler<ConnectionLostEventArgs> ConnectionLost;\n        public event EventHandler ReconnectAttempt;\n        public event EventHandler Disconnected;\n        public event EventHandler Connected;\n\n        public event EventHandler<UserEventArgs> UserAdded;\n        public event EventHandler<UserEventArgs> UserGameIndexUpdated;\n        public event EventHandler<UserNameIndexEventArgs> UserRemoved;\n        public event EventHandler MultipleUsersAdded;\n\n        public CnCNetManager(WindowManager wm, GameCollection gc, CnCNetUserData cncNetUserData, Random random)\n        {\n            gameCollection = gc;\n            this.cncNetUserData = cncNetUserData;\n            connection = new Connection(this, random);\n\n            this.wm = wm;\n\n            cDefaultChatColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.DefaultChatColor);\n\n            ircChatColors = new IRCColor[]\n            {\n                new IRCColor(\"Default color\".L10N(\"Client:Main:ColorDefault\"), false, cDefaultChatColor, 0),\n                new IRCColor(\"Default color #2\".L10N(\"Client:Main:ColorDefault2\"), false, cDefaultChatColor, 1),\n                new IRCColor(\"Light Blue\".L10N(\"Client:Main:ColorLightBlue\"), true, Color.LightBlue, 2),\n                new IRCColor(\"Green\".L10N(\"Client:Main:ColorGreen\"), true, Color.ForestGreen, 3),\n                new IRCColor(\"Dark Red\".L10N(\"Client:Main:ColorDarkRed\"), true, new Color(180, 0, 0, 255), 4),\n                new IRCColor(\"Red\".L10N(\"Client:Main:ColorRed\"), true, Color.Red, 5),\n                new IRCColor(\"Purple\".L10N(\"Client:Main:ColorPurple\"), true, Color.MediumPurple, 6),\n                new IRCColor(\"Orange\".L10N(\"Client:Main:ColorOrange\"), true, Color.Orange, 7),\n                new IRCColor(\"Yellow\".L10N(\"Client:Main:ColorYellow\"), true, Color.Yellow, 8),\n                new IRCColor(\"Lime Green\".L10N(\"Client:Main:ColorLimeGreen\"), true, Color.LimeGreen, 9),\n                new IRCColor(\"Turquoise\".L10N(\"Client:Main:ColorTurquoise\"), true, Color.Turquoise, 10),\n                new IRCColor(\"Sky Blue\".L10N(\"Client:Main:ColorSkyBlue\"), true, Color.LightSkyBlue, 11),\n                new IRCColor(\"Blue\".L10N(\"Client:Main:ColorBlue\"), true, Color.RoyalBlue, 12),\n                new IRCColor(\"Pink\".L10N(\"Client:Main:ColorPink\"), true, Color.DeepPink, 13),\n                new IRCColor(\"Metallic\".L10N(\"Client:Main:ColorLightGrayMetallic\"), true, Color.LightGray, 14),\n                new IRCColor(\"Gray\".L10N(\"Client:Main:ColorGray\"), false, Color.Gray, 15)\n            };\n        }\n\n        public Channel MainChannel { get; private set; }\n\n        private bool connected = false;\n\n        /// <summary>\n        /// Gets a value that determines whether the client is \n        /// currently connected to CnCNet.\n        /// </summary>\n        public bool IsConnected\n        {\n            get { return connected; }\n        }\n\n        public bool IsAttemptingConnection\n        {\n            get { return connection.AttemptingConnection; }\n        }\n\n        /// <summary>\n        /// The list of all users that we can see on the IRC network.\n        /// </summary>\n        public List<IRCUser> UserList = new List<IRCUser>();\n\n        private Connection connection;\n\n        private List<Channel> channels = new List<Channel>();\n\n        private GameCollection gameCollection;\n        private readonly CnCNetUserData cncNetUserData;\n\n        private Color cDefaultChatColor;\n        private IRCColor[] ircChatColors;\n\n        private WindowManager wm;\n\n        private bool disconnect = false;\n\n        public bool IsCnCNetInitialized()\n        {\n            return Connection.IsIdSet();\n        }\n\n        /// <summary>\n        /// Factory method for creating a new channel.\n        /// </summary>\n        /// <param name=\"uiName\">The user-interface name of the channel.</param>\n        /// <param name=\"channelName\">The name of the channel.</param>\n        /// <param name=\"persistent\">Determines whether the channel's information \n        /// should remain in memory even after a disconnect.</param>\n        /// <param name=\"password\">The password for the channel. Use null for none.</param>\n        /// <returns>A channel.</returns>\n        public Channel CreateChannel(string uiName, string channelName,\n            bool persistent, bool isChatChannel, string password)\n        {\n            return new Channel(uiName, channelName, persistent, isChatChannel, password, connection);\n        }\n\n        public void AddChannel(Channel channel)\n        {\n            if (FindChannel(channel.ChannelName) != null)\n                throw new ArgumentException(\"The channel already exists!\".L10N(\"Client:Main:ChannelExist\"), \"channel\");\n\n            channels.Add(channel);\n        }\n\n        public void RemoveChannel(Channel channel)\n        {\n            if (channel.Persistent)\n                throw new ArgumentException(\"Persistent channels cannot be removed.\".L10N(\"Client:Main:PersistentChannelRemove\"), \"channel\");\n\n            channels.Remove(channel);\n        }\n\n        public IRCColor[] GetIRCColors()\n        {\n            return ircChatColors;\n        }\n\n        public void LeaveFromChannel(Channel channel)\n        {\n            connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 10, \"PART \" + channel.ChannelName);\n\n            if (!channel.Persistent)\n                channels.Remove(channel);\n        }\n\n        public void SetMainChannel(Channel channel)\n        {\n            MainChannel = channel;\n        }\n\n        public void SendCustomMessage(QueuedMessage qm)\n        {\n            connection.QueueMessage(qm);\n        }\n\n        public void SendWhoIsMessage(string nick)\n        {\n            SendCustomMessage(new QueuedMessage($\"WHOIS {nick}\", QueuedMessageType.WHOIS_MESSAGE, 0));\n        }\n\n        public void OnAttemptedServerChanged(string serverName)\n        {\n            // AddCallback is necessary for thread-safety; OnAttemptedServerChanged\n            // is called by the networking thread, and AddCallback schedules DoAttemptedServerChanged\n            // to be executed on the main (UI) thread.\n            wm.AddCallback(new Action<string>(DoAttemptedServerChanged), serverName);\n        }\n\n        private void DoAttemptedServerChanged(string serverName)\n        {\n            MainChannel.AddMessage(new ChatMessage(\n                string.Format(\"Attempting connection to {0}\".L10N(\"Client:Main:AttemptConnectToServer\"), serverName)));\n            AttemptedServerChanged?.Invoke(this, new AttemptedServerEventArgs(serverName));\n        }\n\n        public void OnAwayMessageReceived(string userName, string reason)\n        {\n            wm.AddCallback(new Action<string, string>(DoAwayMessageReceived), userName, reason);\n        }\n\n        private void DoAwayMessageReceived(string userName, string reason)\n        {\n            AwayMessageReceived?.Invoke(this, new UserAwayEventArgs(userName, reason));\n        }\n\n        public void OnChannelFull(string channelName)\n        {\n            wm.AddCallback(new Action<string>(DoChannelFull), channelName);\n        }\n\n        private void DoChannelFull(string channelName)\n        {\n            var channel = FindChannel(channelName);\n\n            if (channel != null)\n                channel.OnChannelFull();\n        }\n\n        public void OnTargetChangeTooFast(string channelName, string message)\n        {\n            wm.AddCallback(new Action<string, string>(DoTargetChangeTooFast), channelName, message);\n        }\n\n        private void DoTargetChangeTooFast(string channelName, string message)\n        {\n            var channel = FindChannel(channelName);\n\n            if (channel != null)\n                channel.OnTargetChangeTooFast(message);\n        }\n\n        public void OnChannelInviteOnly(string channelName)\n        {\n            wm.AddCallback(new Action<string>(DoChannelInviteOnly), channelName);\n        }\n\n        private void DoChannelInviteOnly(string channelName)\n        {\n            var channel = FindChannel(channelName);\n\n            if (channel != null)\n                channel.OnInviteOnlyOnJoin();\n        }\n\n        public void OnChannelModesChanged(string userName, string channelName, string modeString, List<string> modeParameters)\n        {\n            wm.AddCallback(new Action<string, string, string, List<string>>(DoChannelModesChanged),\n                userName, channelName, modeString, modeParameters);\n        }\n\n        private void DoChannelModesChanged(string userName, string channelName, string modeString, List<string> modeParameters)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            ApplyChannelModes(channel, modeString, modeParameters);\n\n            channel.OnChannelModesChanged(userName, modeString);\n        }\n\n        private void ApplyChannelModes(Channel channel, string modeString, List<string> modeParameters)\n        {\n            bool addMode = true;\n            int parameterCount = 0;\n            foreach (char modeChar in modeString)\n            {\n                if (modeChar == '+')\n                    addMode = true;\n                else if (modeChar == '-')\n                    addMode = false;\n                else\n                {\n                    switch (modeChar)\n                    {\n                        // Add/remove channel operator status on user.\n                        case 'o':\n                            if (parameterCount >= modeParameters.Count)\n                                break;\n                            string parameter = modeParameters[parameterCount++];\n                            ChannelUser user = channel.Users.Find(parameter);\n                            if (user == null)\n                                break;\n                            user.IsAdmin = addMode;\n                            break;\n\n                        // Add/remove voice status on user.\n                        case 'v':\n                            if (parameterCount >= modeParameters.Count)\n                                break;\n                            string vParam = modeParameters[parameterCount++];\n                            ChannelUser vUser = channel.Users.Find(vParam);\n                            if (vUser == null)\n                                break;\n                            vUser.IRCUser.HasVoice = addMode;\n                            break;\n                    }\n                }\n            }\n        }\n\n        public void OnChannelTopicReceived(string channelName, string topic)\n        {\n            wm.AddCallback(new Action<string, string>(DoChannelTopicReceived), channelName, topic);\n        }\n\n        private void DoChannelTopicReceived(string channelName, string topic)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            channel.Topic = topic;\n        }\n\n        public void OnChannelTopicChanged(string userName, string channelName, string topic)\n        {\n            wm.AddCallback(new Action<string, string>(DoChannelTopicReceived), channelName, topic);\n        }\n\n        public void OnChatMessageReceived(string receiver, string senderName, string ident, string message)\n        {\n            wm.AddCallback(new Action<string, string, string, string>(DoChatMessageReceived),\n                receiver, senderName, ident, message);\n        }\n\n        private void DoChatMessageReceived(string receiver, string senderName, string ident, string message)\n        {\n            Channel channel = FindChannel(receiver);\n\n            if (channel == null)\n                return;\n\n            Color foreColor;\n\n            // Handle ACTION\n            if (message.Contains(\"ACTION\"))\n            {\n                message = message.Remove(0, 7);\n                message = \"====> \" + senderName + \" \" + message;\n                senderName = String.Empty;\n\n                // Replace Funky's game identifiers with real game names\n                for (int i = 0; i < gameCollection.GameList.Count; i++)\n                {\n                    // No localization needed. This message is always in English.\n                    // Only the short game identifier is replaced with the full game name;\n                    // the surrounding \"new ... game\" text is left unmodified.\n                    message = message.Replace(\"new \" + gameCollection.GetGameIdentifierFromIndex(i) + \" game\",\n                        \"new \" + gameCollection.GetFullGameNameFromIndex(i) + \" game\");\n                }\n\n                foreColor = Color.White;\n            }\n            else\n            {\n                // Color parsing\n                if (message.Contains(Convert.ToString((char)03)))\n                {\n                    if (message.Length < 3)\n                    {\n                        foreColor = cDefaultChatColor;\n                    }\n                    else\n                    {\n                        string colorString = message.Substring(1, 2);\n                        message = message.Remove(0, 3);\n                        int colorIndex = Conversions.IntFromString(colorString, -1);\n                        // Try to parse message color info; if fails, use default color\n                        if (colorIndex < ircChatColors.Length && colorIndex > -1)\n                            foreColor = ircChatColors[colorIndex].XnaColor;\n                        else\n                            foreColor = cDefaultChatColor;\n                    }\n                }\n                else\n                    foreColor = cDefaultChatColor;\n            }\n\n            if (message.Length > 1 && message[message.Length - 1] == '\\u001f')\n                message = message.Remove(message.Length - 1);\n\n            ChannelUser user = channel.Users.Find(senderName);\n            bool senderIsAdmin = user != null && user.IsAdmin;\n\n            channel.AddMessage(new ChatMessage(senderName, ident, senderIsAdmin, foreColor, DateTime.Now, message.Replace('\\r', ' ')));\n        }\n\n        public void OnCTCPParsed(string channelName, string userName, string message)\n        {\n            wm.AddCallback(new Action<string, string, string>(DoCTCPParsed),\n                channelName, userName, message);\n        }\n\n        private void DoCTCPParsed(string channelName, string userName, string message)\n        {\n            Channel channel = FindChannel(channelName);\n\n            // it's possible that we received this CTCP via PRIVMSG, in which case we\n            // expect our username instead of a channel as the first parameter\n            if (channel == null)\n            {\n                if (channelName == ProgramConstants.PLAYERNAME)\n                {\n                    PrivateCTCPEventArgs e = new PrivateCTCPEventArgs(userName, message);\n\n                    PrivateCTCPReceived?.Invoke(this, e);\n                }\n\n                return;\n            }\n\n            channel.OnCTCPReceived(userName, message);\n        }\n\n        public void OnConnectAttemptFailed()\n        {\n            wm.AddCallback(new Action(DoConnectAttemptFailed), null);\n        }\n\n        private void DoConnectAttemptFailed()\n        {\n            ConnectAttemptFailed?.Invoke(this, EventArgs.Empty);\n\n            MainChannel.AddMessage(new ChatMessage(Color.Red, \"Connecting to CnCNet failed!\".L10N(\"Client:Main:ConnectToCncNetFailed\")));\n        }\n\n        public void OnConnected()\n        {\n            wm.AddCallback(new Action(DoConnected), null);\n        }\n\n        private void DoConnected()\n        {\n            connected = true;\n            Connected?.Invoke(this, EventArgs.Empty);\n            MainChannel.AddMessage(new ChatMessage(\"Connection to CnCNet established.\".L10N(\"Client:Main:ConnectToCncNetSuccess\")));\n        }\n\n        /// <summary>\n        /// Called when the connection has got cut un-intentionally.\n        /// </summary>\n        /// <param name=\"reason\"></param>\n        public void OnConnectionLost(string reason)\n        {\n            wm.AddCallback(new Action<string>(DoConnectionLost), reason);\n        }\n\n        private void DoConnectionLost(string reason)\n        {\n            ConnectionLost?.Invoke(this, new ConnectionLostEventArgs(reason));\n\n            for (int i = 0; i < channels.Count; i++)\n            {\n                if (!channels[i].Persistent)\n                {\n                    channels.RemoveAt(i);\n                    i--;\n                }\n                else\n                {\n                    channels[i].ClearUsers();\n                }\n            }\n\n            UserList.Clear();\n\n            MainChannel.AddMessage(new ChatMessage(Color.Red, \"Connection to CnCNet has been lost.\".L10N(\"Client:Main:ConnectToCncNetHasLost\")));\n            connected = false;\n        }\n\n        /// <summary>\n        /// Disconnects from CnCNet.\n        /// </summary>\n        public void Disconnect()\n        {\n            connection.Disconnect();\n            disconnect = true;\n        }\n\n        /// <summary>\n        /// Connects to CnCNet.\n        /// </summary>\n        public void Connect()\n        {\n            disconnect = false;\n            MainChannel.AddMessage(new ChatMessage(\"Connecting to CnCNet...\".L10N(\"Client:Main:ConnectingToCncNet\")));\n            connection.ConnectAsync();\n        }\n\n        /// <summary>\n        /// Called when the connection has been aborted intentionally.\n        /// </summary>\n        public void OnDisconnected()\n        {\n            wm.AddCallback(new Action(DoDisconnected), null);\n        }\n\n        private void DoDisconnected()\n        {\n            for (int i = 0; i < channels.Count; i++)\n            {\n                if (!channels[i].Persistent)\n                {\n                    channels.RemoveAt(i);\n                    i--;\n                }\n                else\n                {\n                    channels[i].ClearUsers();\n                }\n            }\n\n            MainChannel.AddMessage(new ChatMessage(\"You have disconnected from CnCNet.\".L10N(\"Client:Main:CncNetDisconnected\")));\n            connected = false;\n\n            UserList.Clear();\n\n            Disconnected?.Invoke(this, EventArgs.Empty);\n        }\n\n        public void OnErrorReceived(string errorMessage)\n        {\n            MainChannel.AddMessage(new ChatMessage(Color.Red, errorMessage));\n        }\n\n        public void OnGenericServerMessageReceived(string message)\n        {\n            wm.AddCallback(new Action<string>(DoGenericServerMessageReceived), message);\n        }\n\n        private void DoGenericServerMessageReceived(string message)\n        {\n            MainChannel.AddMessage(new ChatMessage(message));\n        }\n\n        public void OnIncorrectChannelPassword(string channelName)\n        {\n            wm.AddCallback(new Action<string>(DoIncorrectChannelPassword), channelName);\n        }\n\n        private void DoIncorrectChannelPassword(string channelName)\n        {\n            var channel = FindChannel(channelName);\n            if (channel != null)\n                channel.OnInvalidJoinPassword();\n        }\n\n        public void OnNoticeMessageParsed(string notice, string userName)\n        {\n            // TODO Parse as private message\n        }\n\n        public void OnPrivateMessageReceived(string sender, string message)\n        {\n            wm.AddCallback(new Action<string, string>(DoPrivateMessageReceived),\n                sender, message);\n        }\n\n        private void DoPrivateMessageReceived(string sender, string message)\n        {\n            CnCNetPrivateMessageEventArgs e = new CnCNetPrivateMessageEventArgs(sender, message);\n\n            PrivateMessageReceived?.Invoke(this, e);\n        }\n\n        public void OnReconnectAttempt()\n        {\n            wm.AddCallback(new Action(DoReconnectAttempt), null);\n        }\n\n        private void DoReconnectAttempt()\n        {\n            ReconnectAttempt?.Invoke(this, EventArgs.Empty);\n\n            MainChannel.AddMessage(new ChatMessage(\"Attempting to reconnect to CnCNet...\".L10N(\"Client:Main:ReconnectingCncNet\")));\n\n            connection.ConnectAsync();\n        }\n\n        public void OnUserJoinedChannel(string channelName, string host, string userName, string ident)\n        {\n            wm.AddCallback(new Action<string, string, string, string>(DoUserJoinedChannel),\n                channelName, host, userName, ident);\n        }\n\n        private void DoUserJoinedChannel(string channelName, string host, string userName, string userAddress)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            bool isAdmin = false;\n            string name = userName;\n\n            if (userName.StartsWith(\"@\"))\n            {\n                isAdmin = true;\n                name = userName.Remove(0, 1);\n            }\n\n            IRCUser ircUser = null;\n\n            // Check if we already know this user from another channel\n            // Avoid LINQ here for performance reasons\n            foreach (var user in UserList)\n            {\n                if (user.Name == name)\n                {\n                    ircUser = (IRCUser)user.Clone();\n                    break;\n                }\n            }\n\n            // If we don't know the user, create a new one\n            if (ircUser == null)\n            {\n                string identifier = userAddress.Split('@')[0];\n                string[] parts = identifier.Split('.');\n                ircUser = new IRCUser(name, identifier, host);\n\n                if (parts.Length > 1)\n                {\n                    ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace(\"~\", string.Empty));\n                }\n\n                AddUserToGlobalUserList(ircUser);\n            }\n\n            var channelUser = new ChannelUser(ircUser);\n            channelUser.IsAdmin = isAdmin;\n            channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name);\n\n            ircUser.Channels.Add(channelName);\n            channel.OnUserJoined(channelUser);\n\n            //UserJoinedChannel?.Invoke(this, new ChannelUserEventArgs(channelName, userName));\n        }\n\n        private void AddUserToGlobalUserList(IRCUser user)\n        {\n            UserList.Add(user);\n            UserList = UserList.OrderBy(u => u.Name).ToList();\n            UserAdded?.Invoke(this, new UserEventArgs(user));\n        }\n\n        public void OnUserKicked(string channelName, string userName)\n        {\n            wm.AddCallback(new Action<string, string>(DoUserKicked),\n                channelName, userName);\n        }\n\n        private void DoUserKicked(string channelName, string userName)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            channel.OnUserKicked(userName);\n\n            if (userName == ProgramConstants.PLAYERNAME)\n            {\n                channel.Users.DoForAllUsers(user =>\n                {\n                    RemoveChannelFromUser(user.IRCUser.Name, channelName);\n                });\n\n                if (!channel.Persistent)\n                    channels.Remove(channel);\n\n                channel.ClearUsers();\n                return;\n            }\n\n            RemoveChannelFromUser(userName, channelName);\n        }\n\n        public void OnUserLeftChannel(string channelName, string userName)\n        {\n            wm.AddCallback(new Action<string, string>(DoUserLeftChannel),\n                channelName, userName);\n        }\n\n        private void DoUserLeftChannel(string channelName, string userName)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            channel.OnUserLeft(userName);\n\n            if (userName == ProgramConstants.PLAYERNAME)\n            {\n                channel.Users.DoForAllUsers(user =>\n                {\n                    RemoveChannelFromUser(user.IRCUser.Name, channelName);\n                });\n\n                if (!channel.Persistent)\n                    channels.Remove(channel);\n\n                channel.ClearUsers();\n\n                return;\n            }\n\n            RemoveChannelFromUser(userName, channelName);\n        }\n\n        /// <summary>\n        /// Looks up an user in the global user list and removes a channel from the user.\n        /// If the user is left with 0 channels (meaning we have no common channel with the user),\n        /// the user is removed from the global user list.\n        /// </summary>\n        /// <param name=\"userName\">The name of the user.</param>\n        /// <param name=\"channelName\">The name of the channel.</param>\n        public void RemoveChannelFromUser(string userName, string channelName)\n        {\n            var userIndex = UserList.FindIndex(user => user.Name.ToLower() == userName.ToLower());\n            if (userIndex > -1)\n            {\n                var ircUser = UserList[userIndex];\n                ircUser.Channels.Remove(channelName);\n\n                if (ircUser.Channels.Count == 0)\n                {\n                    UserList.RemoveAt(userIndex);\n                    UserRemoved?.Invoke(this, new UserNameIndexEventArgs(userIndex, userName));\n                }\n            }\n        }\n\n        public void OnUserListReceived(string channelName, string[] userList)\n        {\n            wm.AddCallback(new UserListDelegate(DoUserListReceived),\n                channelName, userList);\n        }\n\n        private void DoUserListReceived(string channelName, string[] userList)\n        {\n            Channel channel = FindChannel(channelName);\n\n            if (channel == null)\n                return;\n\n            var channelUserList = new List<ChannelUser>();\n\n            foreach (string userName in userList)\n            {\n                string name = userName;\n                bool isAdmin = false;\n                bool hasVoice = false;\n\n                if (userName.StartsWith(\"@\"))\n                {\n                    isAdmin = true;\n                    name = userName.Substring(1);\n                }\n                else if (userName.StartsWith(\"+\"))\n                {\n                    hasVoice = true;\n                    name = userName.Substring(1);\n                }\n\n                // Check if we already know the IRC user from another channel\n                IRCUser ircUser = UserList.Find(u => u.Name == name);\n\n                // If the user isn't familiar to us already,\n                // create a new user instance and add it to the global user list\n                if (ircUser == null)\n                {\n                    ircUser = new IRCUser(name);\n                    UserList.Add(ircUser);\n                }\n\n                var channelUser = new ChannelUser(ircUser);\n                channelUser.IsAdmin = isAdmin;\n                channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name);\n                channelUser.IRCUser.HasVoice = hasVoice;\n\n                channelUserList.Add(channelUser);\n            }\n\n            UserList = UserList.OrderBy(u => u.Name).ToList();\n            MultipleUsersAdded?.Invoke(this, EventArgs.Empty);\n\n            channel.OnUserListReceived(channelUserList);\n        }\n\n        public void OnUserQuitIRC(string userName)\n        {\n            wm.AddCallback(new Action<string>(DoUserQuitIRC), userName);\n        }\n\n        private void DoUserQuitIRC(string userName)\n        {\n            new List<Channel>(channels).ForEach(ch => ch.OnUserQuitIRC(userName));\n\n            int userIndex = UserList.FindIndex(user => user.Name == userName);\n\n            if (userIndex > -1)\n            {\n                UserList.RemoveAt(userIndex);\n                UserRemoved?.Invoke(this, new UserNameIndexEventArgs(userIndex, userName));\n            }\n        }\n\n        public void OnWelcomeMessageReceived(string message)\n        {\n            wm.AddCallback(new Action<string>(DoWelcomeMessageReceived), message);\n        }\n\n\n        /// <summary>\n        /// Finds a channel with the specified internal name, case-insensitively.\n        /// </summary>\n        /// <param name=\"channelName\">The internal name of the channel.</param>\n        /// <returns>A channel if one matching the name is found, otherwise null.</returns>\n        public Channel FindChannel(string channelName)\n        {\n            channelName = channelName.ToLower();\n\n            foreach (var channel in channels)\n            {\n                if (channel.ChannelName.ToLower() == channelName)\n                    return channel;\n            }\n\n            return null;\n        }\n\n        private void DoWelcomeMessageReceived(string message)\n        {\n            channels.ForEach(ch => ch.AddMessage(new ChatMessage(message)));\n\n            WelcomeMessageReceived?.Invoke(this, new ServerMessageEventArgs(message));\n        }\n\n        public void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo)\n        {\n            wm.AddCallback(new Action<string, string, string, string>(DoWhoReplyReceived),\n                ident, hostName, userName, extraInfo);\n        }\n\n        private void DoWhoReplyReceived(string ident, string hostName, string userName, string extraInfo)\n        {\n            WhoReplyReceived?.Invoke(this, new WhoEventArgs(ident, userName, extraInfo));\n\n            string[] eInfoParts = extraInfo.Split(' ');\n\n            int gameIndex = -1;\n            if (eInfoParts.Length > 2)\n            {\n                string gameName = eInfoParts[2];\n\n                gameIndex = gameCollection.GetGameIndexFromInternalName(gameName);\n\n                if (gameIndex == -1)\n                    return;\n            }\n\n            var user = UserList.Find(u => u.Name == userName);\n            if (user != null)\n            {\n                user.GameID = gameIndex;\n                user.Ident = ident;\n                user.Hostname = hostName;\n\n                if (gameIndex != -1)\n                {\n                    channels.ForEach(ch => ch.UpdateGameIndexForUser(userName));\n                    UserGameIndexUpdated?.Invoke(this, new UserEventArgs(user));\n                }\n            }\n        }\n\n        public bool GetDisconnectStatus()\n        {\n            return disconnect;\n        }\n\n        public void OnNameAlreadyInUse()\n        {\n            wm.AddCallback(new Action(DoNameAlreadyInUse), null);\n        }\n\n        /// <summary>\n        /// Handles situations when the requested name is already in use by another\n        /// IRC user. Adds additional underscores to the name or replaces existing\n        /// characters with underscores.\n        /// </summary>\n        private void DoNameAlreadyInUse()\n        {\n            var charList = ProgramConstants.PLAYERNAME.ToList();\n            int maxNameLength = ClientConfiguration.Instance.MaxNameLength;\n\n            if (charList.Count < maxNameLength)\n                charList.Add('_');\n            else\n            {\n                int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_');\n\n                if (lastNonUnderscoreIndex == -1)\n                {\n                    MainChannel.AddMessage(new ChatMessage(Color.White,\n                        \"Your nickname is invalid or already in use. Please change your nickname in the login screen.\".L10N(\"Client:Main:PickAnotherNickName\")));\n                    UserINISettings.Instance.SkipConnectDialog.Value = false;\n                    Disconnect();\n                    return;\n                }\n\n                charList[lastNonUnderscoreIndex] = '_';\n            }\n\n            var sb = new StringBuilder();\n            foreach (char c in charList)\n                sb.Append(c);\n\n            MainChannel.AddMessage(new ChatMessage(Color.White,\n                string.Format(\"Your name is already in use. Retrying with {0}...\".L10N(\"Client:Main:NameInUseRetry\"), sb.ToString())));\n\n            ProgramConstants.PLAYERNAME = sb.ToString();\n            connection.ChangeNickname();\n        }\n\n        public void OnBannedFromChannel(string channelName)\n        {\n            wm.AddCallback(new Action<string>(DoBannedFromChannel), channelName);\n        }\n\n        private void DoBannedFromChannel(string channelName)\n        {\n            BannedFromChannel?.Invoke(this, new ChannelEventArgs(channelName));\n        }\n\n        public void OnUserNicknameChange(string oldNickname, string newNickname)\n            => wm.AddCallback(new Action<string, string>(DoUserNicknameChange), oldNickname, newNickname);\n\n        private void DoUserNicknameChange(string oldNickname, string newNickname)\n        {\n            IRCUser user = UserList.Find(u => u.Name.ToUpper() == oldNickname.ToUpper());\n            if (user == null)\n            {\n                Logger.Log(\"DoUserNicknameChange: Failed to find user with nickname \" + oldNickname);\n                return;\n            }\n            string realOldNickname = user.Name; // To make sure that case matches\n            user.Name = newNickname;\n\n            channels.ForEach(ch => ch.OnUserNameChanged(realOldNickname, newNickname));\n        }\n\n        public void OnServerLatencyTested(int candidateCount, int closerCount)\n        {\n            wm.AddCallback(new Action<int, int>(DoServerLatencyTested), candidateCount, closerCount);\n        }\n\n        private void DoServerLatencyTested(int candidateCount, int closerCount)\n        {\n            MainChannel.AddMessage(new ChatMessage(\n                string.Format(\n                    \"Lobby servers: {0} available, {1} fast.\".L10N(\"Client:Main:LobbyServerLatencyTestResult\"),\n                    candidateCount, closerCount)));\n        }\n    }\n\n    public class UserEventArgs : EventArgs\n    {\n        public UserEventArgs(IRCUser ircUser)\n        {\n            User = ircUser;\n        }\n\n        public IRCUser User { get; private set; }\n    }\n\n    public class IndexEventArgs : EventArgs\n    {\n        public IndexEventArgs(int index)\n        {\n            Index = index;\n        }\n\n        public int Index { get; private set; }\n    }\n\n    public class UserNameChangedEventArgs : EventArgs\n    {\n        public UserNameChangedEventArgs(string oldUserName, IRCUser user)\n        {\n            OldUserName = oldUserName;\n            User = user;\n        }\n\n        public string OldUserName { get; }\n        public IRCUser User { get; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/CnCNetUserData.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nusing ClientCore;\n\nusing Rampastring.Tools;\nusing Rampastring.XNAUI;\n\nnamespace DTAClient.Online\n{\n    public sealed class CnCNetUserData\n    {\n        private const string FRIEND_LIST_PATH = \"Client/friend_list\";\n        private const string IGNORE_LIST_PATH = \"Client/ignore_list\";\n        private const string RECENT_LIST_PATH = \"Client/recent_list\";\n\n        private const int RECENT_LIMIT = 50;\n\n        private readonly Lazy<List<string>> lazyFriendList;\n\n        /// <summary>\n        /// A list which contains names of friended users. If you manipulate this list\n        /// directly you have to also invoke UserFriendToggled event handler for every\n        /// user name added or removed.\n        /// </summary>\n        public List<string> FriendList => lazyFriendList.Value;\n\n        private readonly Lazy<List<string>> lazyIgnoreList;\n\n        /// <summary>\n        /// A list which contains idents of ignored users. If you manipulate this list\n        /// directly you have to also invoke UserIgnoreToggled event handler for every\n        /// user ident added or removed.\n        /// </summary>\n        public List<string> IgnoreList => lazyIgnoreList.Value;\n\n        private readonly Lazy<List<RecentPlayer>> lazyRecentList;\n\n        /// <summary>\n        /// A list which contains names of players from recent games.\n        /// </summary>\n        public List<RecentPlayer> RecentList => lazyRecentList.Value;\n\n        public event EventHandler<UserNameEventArgs> UserFriendToggled;\n        public event EventHandler<IdentEventArgs> UserIgnoreToggled;\n\n        public CnCNetUserData(WindowManager windowManager)\n        {\n            lazyFriendList = new Lazy<List<string>>(LoadFriendList, LazyThreadSafetyMode.ExecutionAndPublication);\n            lazyIgnoreList = new Lazy<List<string>>(LoadIgnoreList, LazyThreadSafetyMode.ExecutionAndPublication);\n            lazyRecentList = new Lazy<List<RecentPlayer>>(LoadRecentPlayerList, LazyThreadSafetyMode.ExecutionAndPublication);\n\n            // Load lists in background. Fire-and-forget.\n            Task.Run(() => _ = FriendList);\n            Task.Run(() => _ = IgnoreList);\n            Task.Run(() => _ = RecentList);\n\n            windowManager.GameClosing += WindowManager_GameClosing;\n        }\n\n        private static List<string> LoadTextList(string path)\n        {\n            try\n            {\n                FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path);\n\n                if (listFile.Exists)\n                    return File.ReadAllLines(listFile.FullName).ToList();\n\n                Logger.Log($\"Loading {path} failed! File does not exist.\");\n                return new();\n            }\n            catch\n            {\n                Logger.Log($\"Loading {path} list failed!\");\n                return new();\n            }\n        }\n\n        private static List<T> LoadJsonList<T>(string path)\n        {\n            try\n            {\n                FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path);\n\n                if (listFile.Exists)\n                    return JsonSerializer.Deserialize<List<T>>(File.ReadAllText(listFile.FullName)) ?? new List<T>();\n\n                Logger.Log($\"Loading {path} failed! File does not exist.\");\n                return new();\n            }\n            catch\n            {\n                Logger.Log($\"Loading {path} list failed!\");\n                return new();\n            }\n        }\n\n        private static void SaveTextList(string path, List<string> textList)\n        {\n            Logger.Log($\"Saving {path}.\");\n\n            try\n            {\n                FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path);\n\n                listFileInfo.Delete();\n                File.WriteAllLines(listFileInfo.FullName, textList.ToArray());\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"Saving {path} failed! Error message: \" + ex.ToString());\n            }\n        }\n\n        private static void SaveJsonList<T>(string path, IReadOnlyCollection<T> jsonList)\n        {\n            Logger.Log($\"Saving {path}.\");\n\n            try\n            {\n                FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path);\n\n                listFileInfo.Delete();\n                File.WriteAllText(listFileInfo.FullName, JsonSerializer.Serialize(jsonList));\n            }\n            catch (Exception ex)\n            {\n                Logger.Log($\"Saving {path} failed! Error message: \" + ex.ToString());\n            }\n        }\n\n        private static void Toggle(string value, ICollection<string> list)\n        {\n            if (string.IsNullOrEmpty(value))\n                return;\n\n            if (list.Contains(value))\n                list.Remove(value);\n            else\n                list.Add(value);\n        }\n\n        private List<string> LoadFriendList() => LoadTextList(FRIEND_LIST_PATH);\n\n        private List<string> LoadIgnoreList() => LoadTextList(IGNORE_LIST_PATH);\n\n        private List<RecentPlayer> LoadRecentPlayerList() => LoadJsonList<RecentPlayer>(RECENT_LIST_PATH);\n\n        private void WindowManager_GameClosing(object sender, EventArgs e) => Save();\n\n        private void SaveFriends() => SaveTextList(FRIEND_LIST_PATH, FriendList);\n\n        private void SaveIgnoreList() => SaveTextList(IGNORE_LIST_PATH, IgnoreList);\n\n        private void SaveRecentList() => SaveJsonList(RECENT_LIST_PATH, RecentList);\n\n        private void Save()\n        {\n            SaveFriends();\n            SaveIgnoreList();\n            SaveRecentList();\n        }\n\n        /// <summary>\n        /// Adds or removes a specified user to or from the friend list\n        /// depending on whether they already are on the friend list.\n        /// </summary>\n        /// <param name=\"name\">The name of the user.</param>\n        public void ToggleFriend(string name)\n        {\n            Toggle(name, FriendList);\n            UserFriendToggled?.Invoke(this, new(name));\n        }\n\n        /// <summary>\n        /// Adds or removes a specified user to or from the chat ignore list\n        /// depending on whether they already are on the ignore list.\n        /// </summary>\n        /// <param name=\"ident\">The ident of the IRCUser.</param>\n        public void ToggleIgnoreUser(string ident)\n        {\n            Toggle(ident, IgnoreList);\n            UserIgnoreToggled?.Invoke(this, new(ident));\n        }\n\n        public void AddRecentPlayers(IEnumerable<string> recentPlayerNames, string gameName)\n        {\n            recentPlayerNames = recentPlayerNames.Where(name => name != ProgramConstants.PLAYERNAME);\n            var now = DateTime.UtcNow;\n            RecentList.AddRange(recentPlayerNames.Select(rp => new RecentPlayer()\n            {\n                PlayerName = rp,\n                GameName = gameName,\n                GameTime = now\n            }));\n            int skipCount = Math.Max(0, RecentList.Count - RECENT_LIMIT);\n            RecentList.RemoveRange(0, skipCount);\n        }\n\n        /// <summary>\n        /// Checks to see if a user is in the ignore list.\n        /// </summary>\n        /// <param name=\"ident\">The IRC identifier of the user.</param>\n        public bool IsIgnored(string ident) => IgnoreList.Contains(ident);\n\n        /// <summary>\n        /// Checks if a specified user belongs to the friend list.\n        /// </summary>\n        /// <param name=\"name\">The name of the user.</param>\n        public bool IsFriend(string name) => FriendList.Contains(name);\n    }\n\n    public sealed class IdentEventArgs : EventArgs\n    {\n        public IdentEventArgs(string ident)\n        {\n            Ident = ident;\n        }\n\n        public string Ident { get; }\n    }\n}"
  },
  {
    "path": "DXMainClient/Online/Connection.cs",
    "content": "using ClientCore;\nusing ClientCore.Extensions;\nusing Rampastring.Tools;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.NetworkInformation;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// The CnCNet connection handler.\n    /// </summary>\n    public class Connection\n    {\n        private const int MAX_RECONNECT_COUNT = 8;\n        private const int MAX_ERROR_COUNT = 30;\n        private const int RECONNECT_WAIT_DELAY = 4000;\n        private const int ID_LENGTH = 9;\n        private const int MAXIMUM_LATENCY = 400;\n        private const int BYTE_ARRAY_MSG_LEN = 1024;\n\n        public Connection(IConnectionManager connectionManager, Random random)\n        {\n            this.connectionManager = connectionManager;\n            this.Rng = random;\n        }\n\n        IConnectionManager connectionManager;\n\n        public Random Rng;\n\n        private static IList<Server> _servers = null;\n        /// <summary>\n        /// The list of CnCNet / GameSurge IRC servers to connect to.\n        /// </summary>\n        private static IList<Server> Servers\n        {\n            get\n            {\n                if (_servers is not null)\n                    return _servers;\n\n                IEnumerable<string> serversList;\n                if (ClientConfiguration.Instance.IRCServers.Count > 0)\n                    serversList = ClientConfiguration.Instance.IRCServers;\n                else\n                {\n                    // fallback to the hardcoded servers list\n                    serversList = [\n                        \"irc.gamesurge.net|GameSurge|6667,6660,6666,6668,6669\",\n                    ];\n                }\n\n                _servers = serversList.Select(Server.Deserialize).ToList();\n                return _servers;\n            }\n        }\n\n        bool _isConnected = false;\n        public bool IsConnected\n        {\n            get { return _isConnected; }\n        }\n\n        bool _attemptingConnection = false;\n        public bool AttemptingConnection\n        {\n            get { return _attemptingConnection; }\n        }\n\n        private List<QueuedMessage> MessageQueue = new List<QueuedMessage>();\n        private TimeSpan MessageQueueDelay;\n\n        private NetworkStream serverStream;\n        private TcpClient tcpClient;\n\n        volatile int reconnectCount = 0;\n\n        private volatile bool connectionCut = false;\n        private volatile bool welcomeMessageReceived = false;\n        private volatile bool sendQueueExited = false;\n        bool _disconnect = false;\n        private bool disconnect\n        {\n            get\n            {\n                lock (locker)\n                    return _disconnect;\n            }\n            set\n            {\n                lock (locker)\n                    _disconnect = value;\n            }\n        }\n\n        private string overMessage;\n\n        private readonly Encoding encoding = Encoding.UTF8;\n\n        /// <summary>\n        /// A list of server IPs that have dropped our connection.\n        /// The client skips these servers when attempting to re-connect, to\n        /// prevent a server that first accepts a connection and then drops it\n        /// right afterwards from preventing online play.\n        /// </summary>\n        private readonly List<string> failedServerIPs = new List<string>();\n        private volatile string currentConnectedServerIP;\n\n        private static readonly object locker = new object();\n        private static readonly object messageQueueLocker = new object();\n\n        private static bool idSet = false;\n        private static string systemId;\n        private static readonly object idLocker = new object();\n\n        public static void SetId(string id)\n        {\n            lock (idLocker)\n            {\n                int maxLength = ID_LENGTH - (ClientConfiguration.Instance.LocalGame.Length + 1);\n                systemId = Utilities.CalculateSHA1ForString(id).Substring(0, maxLength);\n                idSet = true;\n            }\n        }\n\n        public static bool IsIdSet()\n        {\n            lock (idLocker)\n            {\n                return idSet;\n            }\n        }\n\n        /// <summary>\n        /// Attempts to connect to CnCNet without blocking the calling thread.\n        /// </summary>\n        public void ConnectAsync()\n        {\n            if (_isConnected)\n                throw new InvalidOperationException(\"The client is already connected!\");\n\n            if (_attemptingConnection)\n                return; // Maybe we should throw in this case as well?\n\n            welcomeMessageReceived = false;\n            connectionCut = false;\n            _attemptingConnection = true;\n            disconnect = false;\n\n            MessageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep);\n\n            Thread connection = new Thread(ConnectToServer);\n            connection.Start();\n        }\n\n        /// <summary>\n        /// Attempts to connect to CnCNet.\n        /// </summary>\n        private void ConnectToServer()\n        {\n            IList<Server> sortedServerList = GetServerListSortedByLatency();\n\n            foreach (Server server in sortedServerList)\n            {\n                try\n                {\n                    for (int i = 0; i < server.Ports.Length; i++)\n                    {\n                        connectionManager.OnAttemptedServerChanged(server.Name);\n\n                        TcpClient client = new TcpClient(AddressFamily.InterNetwork);\n                        var result = client.BeginConnect(server.Host, server.Ports[i], null, null);\n                        result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3), false);\n\n                        Logger.Log(\"Attempting connection to \" + server.Host + \":\" + server.Ports[i]);\n\n                        if (!client.Connected)\n                        {\n                            Logger.Log(\"Connecting to \" + server.Host + \" port \" + server.Ports[i] + \" timed out!\");\n                            continue; // Start all over again, using the next port\n                        }\n\n                        Logger.Log(\"Succesfully connected to \" + server.Host + \" on port \" + server.Ports[i]);\n                        client.EndConnect(result);\n\n                        _isConnected = true;\n                        _attemptingConnection = false;\n\n                        connectionManager.OnConnected();\n\n                        Thread sendQueueHandler = new Thread(RunSendQueue);\n                        sendQueueHandler.Start();\n\n                        tcpClient = client;\n                        serverStream = tcpClient.GetStream();\n                        serverStream.ReadTimeout = 1000;\n\n                        currentConnectedServerIP = server.Host;\n                        HandleComm();\n                        return;\n                    }\n                }\n                catch (Exception ex)\n                {\n                    Logger.Log(\"Unable to connect to the server. \" + ex.ToString());\n                }\n            }\n\n            Logger.Log(\"Connecting to CnCNet failed!\");\n            // Clear the failed server list in case connecting to all servers has failed\n            failedServerIPs.Clear();\n            _attemptingConnection = false;\n            connectionManager.OnConnectAttemptFailed();\n        }\n\n        private void HandleComm()\n        {\n            int errorTimes = 0;\n            byte[] message = new byte[BYTE_ARRAY_MSG_LEN];\n\n            Register();\n\n            Timer timer = new Timer(AutoPing, null, 30000, 120000);\n\n            connectionCut = true;\n\n            while (true)\n            {\n                if (connectionManager.GetDisconnectStatus())\n                {\n                    connectionManager.OnDisconnected();\n                    connectionCut = false; // This disconnect is intentional\n                    break;\n                }\n\n                int bytesRead = 0;\n\n                try\n                {\n                    bytesRead = serverStream.Read(message, 0, BYTE_ARRAY_MSG_LEN);\n                }\n                catch (IOException ex)\n                {\n                    errorTimes++;\n\n                    if (errorTimes > MAX_ERROR_COUNT)\n                    {\n                        const string errorMessage = \"Disconnected from CnCNet after not receiving a packet for too long.\";\n                        Logger.Log(errorMessage + Environment.NewLine + \"Message: \" + ex.ToString());\n                        failedServerIPs.Add(currentConnectedServerIP);\n                        connectionManager.OnConnectionLost(errorMessage.L10N(\"Client:Main:ClientDisconnectedAfterRetries\"));\n                        break;\n                    }\n\n                    continue;\n                }\n                catch (Exception ex)\n                {\n                    const string errorMessage = \"Disconnected from CnCNet due to an internal error.\";\n                    Logger.Log(errorMessage + Environment.NewLine + \"Message: \" + ex.ToString());\n                    failedServerIPs.Add(currentConnectedServerIP);\n                    connectionManager.OnConnectionLost(errorMessage.L10N(\"Client:Main:ClientDisconnectedAfterException\"));\n                    break;\n                }\n\n                if (bytesRead == 0)\n                {\n                    errorTimes++;\n\n                    if (errorTimes > MAX_ERROR_COUNT)\n                    {\n                        Logger.Log(\"Disconnected from CnCNet.\");\n                        failedServerIPs.Add(currentConnectedServerIP);\n                        connectionManager.OnConnectionLost(\"Server disconnected.\".L10N(\"Client:Main:ServerDisconnected\"));\n                        break;\n                    }\n\n                    continue;\n                }\n\n                errorTimes = 0;\n\n                // A message has been succesfully received\n                string msg = encoding.GetString(message, 0, bytesRead);\n                Logger.Log(\"Message received: \" + msg);\n\n                HandleMessage(msg);\n                timer.Change(30000, 30000);\n            }\n\n            timer.Change(Timeout.Infinite, Timeout.Infinite);\n            timer.Dispose();\n\n            _isConnected = false;\n            disconnect = false;\n\n            if (connectionCut)\n            {\n                while (!sendQueueExited)\n                    Thread.Sleep(100);\n\n                reconnectCount++;\n\n                if (reconnectCount > MAX_RECONNECT_COUNT)\n                {\n                    Logger.Log(\"Reconnect attempt count exceeded!\");\n                    return;\n                }\n\n                Thread.Sleep(RECONNECT_WAIT_DELAY);\n\n                if (IsConnected || AttemptingConnection)\n                {\n                    Logger.Log(\"Cancelling reconnection attempt because the user has attempted to reconnect manually.\");\n                    return;\n                }\n\n                Logger.Log(\"Attempting to reconnect to CnCNet.\");\n                connectionManager.OnReconnectAttempt();\n            }\n        }\n\n        /// <summary>\n        /// Get all IP addresses of Lobby servers by resolving the hostname and test the latency to the servers.\n        /// The maximum latency is defined in <c>MAXIMUM_LATENCY</c>, see <see cref=\"Connection.MAXIMUM_LATENCY\"/>.\n        /// Servers that did not respond to ICMP messages in time will be placed at the end of the list.\n        /// </summary>\n        /// <returns>A list of Lobby servers sorted by latency.</returns>\n        private IList<Server> GetServerListSortedByLatency()\n        {\n            // Resolve the hostnames.\n            ICollection<Task<IEnumerable<Tuple<IPAddress, string, int[]>>>>\n                dnsTasks = new List<Task<IEnumerable<Tuple<IPAddress, string, int[]>>>>(Servers.Count);\n\n            foreach (Server server in Servers)\n            {\n                string serverHostnameOrIPAddress = server.Host;\n                string serverName = server.Name;\n                int[] serverPorts = server.Ports;\n\n                Task<IEnumerable<Tuple<IPAddress, string, int[]>>> dnsTask = new Task<IEnumerable<Tuple<IPAddress, string, int[]>>>(() =>\n                {\n                    Logger.Log($\"Attempting to DNS resolve {serverName} ({serverHostnameOrIPAddress}).\");\n                    ICollection<Tuple<IPAddress, string, int[]>> _serverInfos = new List<Tuple<IPAddress, string, int[]>>();\n\n                    try\n                    {\n                        // If hostNameOrAddress is an IP address, this address is returned without querying the DNS server.\n                        IEnumerable<IPAddress> serverIPAddresses = Dns.GetHostAddresses(serverHostnameOrIPAddress)\n                                                                      .Where(IPAddress => IPAddress.AddressFamily == AddressFamily.InterNetwork);\n\n                        Logger.Log($\"DNS resolved {serverName} ({serverHostnameOrIPAddress}): \" +\n                            $\"{string.Join(\", \", serverIPAddresses.Select(item => item.ToString()))}\");\n\n                        // Store each IPAddress in a different tuple.\n                        foreach (IPAddress serverIPAddress in serverIPAddresses)\n                        {\n                            _serverInfos.Add(new Tuple<IPAddress, string, int[]>(serverIPAddress, serverName, serverPorts));\n                        }\n                    }\n                    catch (SocketException ex)\n                    {\n                        Logger.Log($\"Caught an exception when DNS resolving {serverName} ({serverHostnameOrIPAddress}) Lobby server: {ex.ToString()}\");\n                    }\n\n                    return _serverInfos;\n                });\n\n                dnsTask.Start();\n                dnsTasks.Add(dnsTask);\n            }\n\n            Task.WaitAll(dnsTasks.ToArray());\n\n            // Group the tuples by IPAddress to merge duplicate servers.\n            IEnumerable<IGrouping<IPAddress, Tuple<string, int[]>>>\n                serverInfosGroupedByIPAddress = dnsTasks.SelectMany(dnsTask => dnsTask.Result)      // Tuple<IPAddress, serverName, serverPorts>\n                                                        .GroupBy(\n                                                            serverInfo => serverInfo.Item1,         // IPAddress\n                                                            serverInfo => new Tuple<string, int[]>(\n                                                                serverInfo.Item2,                   // serverName\n                                                                serverInfo.Item3                    // serverPorts\n                                                            )\n                                                        );\n\n            // Process each group:\n            //   1. Get IPAddress.\n            //   2. Concatenate serverNames.\n            //   3. Remove duplicate ports.\n            //   4. Construct and return a tuple that contains the IPAddress, concatenated serverNames and unique ports.\n            IEnumerable<Tuple<IPAddress, string, int[]>> serverInfos = serverInfosGroupedByIPAddress.Select(serverInfoGroup =>\n            {\n                IPAddress ipAddress = serverInfoGroup.Key;\n                string serverNames = string.Join(\", \", serverInfoGroup.Select(serverInfo => serverInfo.Item1));\n                int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Item2).Distinct().ToArray();\n\n                return new Tuple<IPAddress, string, int[]>(ipAddress, serverNames, serverPorts);\n            });\n\n            // Do logging.\n            foreach (Tuple<IPAddress, string, int[]> serverInfo in serverInfos)\n            {\n                string serverIPAddress = serverInfo.Item1.ToString();\n                string serverNames = string.Join(\", \", serverInfo.Item2.ToString());\n                string serverPorts = string.Join(\", \", serverInfo.Item3.Select(port => port.ToString()));\n\n                Logger.Log($\"Got a Lobby server. IP: {serverIPAddress}; Name: {serverNames}; Ports: {serverPorts}.\");\n            }\n\n            Logger.Log($\"The number of Lobby servers is {serverInfos.Count()}.\");\n\n            // Test the latency.\n            ICollection<Task<Tuple<Server, long>>> pingTasks = new List<Task<Tuple<Server, long>>>(serverInfos.Count());\n\n            foreach (Tuple<IPAddress, string, int[]> serverInfo in serverInfos)\n            {\n                IPAddress serverIPAddress = serverInfo.Item1;\n                string serverNames = serverInfo.Item2;\n                int[] serverPorts = serverInfo.Item3;\n\n                if (failedServerIPs.Contains(serverIPAddress.ToString()))\n                {\n                    Logger.Log($\"Skipped a failed server {serverNames} ({serverIPAddress}).\");\n                    continue;\n                }\n\n                Task<Tuple<Server, long>> pingTask = new Task<Tuple<Server, long>>(() =>\n                {\n                    Logger.Log($\"Attempting to ping {serverNames} ({serverIPAddress}).\");\n                    Server server = new Server(serverIPAddress.ToString(), serverNames, serverPorts);\n\n                    using (Ping ping = new Ping())\n                    {\n                        try\n                        {\n                            PingReply pingReply = ping.Send(serverIPAddress, MAXIMUM_LATENCY);\n\n                            if (pingReply.Status == IPStatus.Success)\n                            {\n                                long pingInMs = pingReply.RoundtripTime;\n                                Logger.Log($\"The latency in milliseconds to the server {serverNames} ({serverIPAddress}): {pingInMs}.\");\n\n                                return new Tuple<Server, long>(server, pingInMs);\n                            }\n                            else\n                            {\n                                Logger.Log($\"Failed to ping the server {serverNames} ({serverIPAddress}): \" +\n                                    $\"{Enum.GetName(typeof(IPStatus), pingReply.Status)}.\");\n\n                                return new Tuple<Server, long>(server, long.MaxValue);\n                            }\n                        }\n                        catch (PingException ex)\n                        {\n                            Logger.Log($\"Caught an exception when pinging {serverNames} ({serverIPAddress}) Lobby server: {ex.ToString()}\");\n\n                            return new Tuple<Server, long>(server, long.MaxValue);\n                        }\n                    }\n                });\n\n                pingTask.Start();\n                pingTasks.Add(pingTask);\n            }\n\n            Task.WaitAll(pingTasks.ToArray());\n\n            // Sort the servers by latency.\n            IOrderedEnumerable<Tuple<Server, long>>\n                sortedServerAndLatencyResults = pingTasks.Select(task => task.Result)              // Tuple<Server, Latency>\n                                                         .OrderBy(taskResult => taskResult.Item2); // Latency\n\n            // Do logging.\n            foreach (Tuple<Server, long> serverAndLatencyResult in sortedServerAndLatencyResults)\n            {\n                string serverIPAddress = serverAndLatencyResult.Item1.Host;\n                long serverLatencyValue = serverAndLatencyResult.Item2;\n                string serverLatencyString = serverLatencyValue <= MAXIMUM_LATENCY ? serverLatencyValue.ToString() : \"DNF\";\n\n                Logger.Log($\"Lobby server IP: {serverIPAddress}, latency: {serverLatencyString}.\");\n            }\n\n            {\n                int candidateCount = sortedServerAndLatencyResults.Count();\n                int closerCount = sortedServerAndLatencyResults.Count(\n                    serverAndLatencyResult => serverAndLatencyResult.Item2 <= MAXIMUM_LATENCY);\n\n                Logger.Log($\"Lobby servers: {candidateCount} available, {closerCount} fast.\");\n                connectionManager.OnServerLatencyTested(candidateCount, closerCount);\n            }\n\n            return sortedServerAndLatencyResults.Select(taskResult => taskResult.Item1).ToList(); // Server\n        }\n\n        public void Disconnect()\n        {\n            disconnect = true;\n            SendMessage(\"QUIT\");\n\n            tcpClient.Close();\n            serverStream.Close();\n        }\n\n        #region Handling commands\n\n        /// <summary>\n        /// Checks if a message from the IRC server is a partial or full\n        /// message, and handles it accordingly.\n        /// </summary>\n        /// <param name=\"message\">The message.</param>\n        private void HandleMessage(string message)\n        {\n            string msg = overMessage + message;\n            overMessage = \"\";\n            while (true)\n            {\n                int commandEndIndex = msg.IndexOf(\"\\n\");\n\n                if (commandEndIndex == -1)\n                {\n                    overMessage = msg;\n                    break;\n                }\n                else if (msg.Length != commandEndIndex + 1)\n                {\n                    string command = msg.Substring(0, commandEndIndex - 1);\n                    PerformCommand(command);\n\n                    msg = msg.Remove(0, commandEndIndex + 1);\n                }\n                else\n                {\n                    string command = msg.Substring(0, msg.Length - 1);\n                    PerformCommand(command);\n                    break;\n                }\n            }\n        }\n\n        /// <summary>\n        /// Handles a specific command received from the IRC server.\n        /// </summary>\n        private void PerformCommand(string message)\n        {\n            string prefix = String.Empty;\n            string command = String.Empty;\n            message = message.Replace(\"\\r\", String.Empty);\n            List<string> parameters = new List<string>();\n            ParseIrcMessage(message, out prefix, out command, out parameters);\n            string paramString = String.Empty;\n            foreach (string param in parameters) { paramString = paramString + param + \",\"; }\n            Logger.Log(\"RMP: \" + prefix + \" \" + command + \" \" + paramString);\n\n            try\n            {\n                bool success = false;\n                int commandNumber = -1;\n                success = Int32.TryParse(command, out commandNumber);\n\n                if (success)\n                {\n                    string serverMessagePart = prefix + \": \";\n\n                    switch (commandNumber)\n                    {\n                        // Command descriptions from https://www.alien.net.au/irc/irc2numerics.html\n\n                        case 001: // Welcome message\n                            message = serverMessagePart + parameters[1];\n                            welcomeMessageReceived = true;\n                            connectionManager.OnWelcomeMessageReceived(message);\n                            reconnectCount = 0;\n                            break;\n                        case 002: // \"Your host is x, running version y\"\n                        case 003: // \"This server was created...\"\n                        case 251: // There are <int> users and <int> invisible on <int> servers\n                        case 255: // I have <int> clients and <int> servers\n                        case 265: // Local user count\n                        case 266: // Global user count\n                        case 401: // Used to indicate the nickname parameter supplied to a command is currently unused\n                        case 403: // Used to indicate the given channel name is invalid, or does not exist\n                        case 404: // Used to indicate that the user does not have the rights to send a message to a channel\n                        case 432: // Invalid nickname on registration\n                        case 461: // Returned by the server to any command which requires more parameters than the number of parameters given\n                        case 465: // Returned to a client after an attempt to register on a server configured to ban connections from that client\n                            StringBuilder displayedMessage = new StringBuilder(serverMessagePart);\n                            for (int i = 1; i < parameters.Count; i++)\n                            {\n                                displayedMessage.Append(' ');\n                                displayedMessage.Append(parameters[i]);\n                            }\n                            connectionManager.OnGenericServerMessageReceived(displayedMessage.ToString());\n                            break;\n                        case 439: // Attempt to send messages too fast\n                            connectionManager.OnTargetChangeTooFast(parameters[1], parameters[2]);\n                            break;\n                        case 252: // Number of operators online\n                        case 254: // Number of channels formed\n                            message = serverMessagePart + parameters[1] + \" \" + parameters[2];\n                            connectionManager.OnGenericServerMessageReceived(message);\n                            break;\n                        case 301: // AWAY message\n                            string awayTarget = parameters[0];\n                            if (awayTarget != ProgramConstants.PLAYERNAME)\n                                break;\n                            string awayPlayer = parameters[1];\n                            string awayReason = parameters[2];\n                            connectionManager.OnAwayMessageReceived(awayPlayer, awayReason);\n                            break;\n                        case 332: // Channel topic message\n                            string _target = parameters[0];\n                            if (_target != ProgramConstants.PLAYERNAME)\n                                break;\n                            connectionManager.OnChannelTopicReceived(parameters[1], parameters[2]);\n                            break;\n                        case 353: // User list (reply to NAMES)\n                            string target = parameters[0];\n                            if (target != ProgramConstants.PLAYERNAME)\n                                break;\n                            string channelName = parameters[2];\n                            string[] users = parameters[3].Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);\n                            connectionManager.OnUserListReceived(channelName, users);\n                            break;\n                        case 352: // Reply to WHO query\n                            string ident = parameters[2];\n                            string host = parameters[3];\n                            string wUserName = parameters[5];\n                            string extraInfo = parameters[7];\n                            connectionManager.OnWhoReplyReceived(ident, host, wUserName, extraInfo);\n                            break;\n                        case 311: // Reply to WHOIS NAME query\n                            connectionManager.OnWhoReplyReceived(parameters[2], parameters[3], parameters[1], string.Empty);\n                            break;\n                        case 433: // Name already in use\n                            message = serverMessagePart + parameters[1] + \": \" + parameters[2];\n                            //connectionManager.OnGenericServerMessageReceived(message);\n                            connectionManager.OnNameAlreadyInUse();\n                            break;\n                        case 451: // Not registered\n                            Register();\n                            connectionManager.OnGenericServerMessageReceived(message);\n                            break;\n                        case 471: // Returned when attempting to join a channel that is full (basically, player limit met)\n                            connectionManager.OnChannelFull(parameters[1]);\n                            break;\n                        case 473: // Returned when attempting to join an invite-only channel (locked games)\n                            connectionManager.OnChannelInviteOnly(parameters[1]);\n                            break;\n                        case 474: // Returned when attempting to join a channel a user is banned from\n                            connectionManager.OnBannedFromChannel(parameters[1]);\n                            break;\n                        case 475: // Returned when attempting to join a key-locked channel either without a key or with the wrong key\n                            connectionManager.OnIncorrectChannelPassword(parameters[1]);\n                            break;\n                    }\n\n                    return;\n                }\n\n                switch (command)\n                {\n                    case \"NOTICE\":\n                        int noticeExclamIndex = prefix.IndexOf('!');\n                        if (noticeExclamIndex > -1)\n                        {\n                            if (parameters.Count > 1 && parameters[1][0] == 1)//Conversions.IntFromString(parameters[1].Substring(0, 1), -1) == 1)\n                            {\n                                // CTCP\n                                string channelName = parameters[0];\n                                string ctcpMessage = parameters[1];\n                                ctcpMessage = ctcpMessage.Remove(0, 1).Remove(ctcpMessage.Length - 2);\n                                string ctcpSender = prefix.Substring(0, noticeExclamIndex);\n                                connectionManager.OnCTCPParsed(channelName, ctcpSender, ctcpMessage);\n\n                                return;\n                            }\n                            else\n                            {\n                                string noticeUserName = prefix.Substring(0, noticeExclamIndex);\n                                string notice = parameters[parameters.Count - 1];\n                                connectionManager.OnNoticeMessageParsed(notice, noticeUserName);\n                                break;\n                            }\n                        }\n                        string noticeParamString = String.Empty;\n                        foreach (string param in parameters)\n                            noticeParamString = noticeParamString + param + \" \";\n                        connectionManager.OnGenericServerMessageReceived(prefix + \" \" + noticeParamString);\n                        break;\n                    case \"JOIN\":\n                        string channel = parameters[0];\n                        int atIndex = prefix.IndexOf('@');\n                        int exclamIndex = prefix.IndexOf('!');\n                        string userName = prefix.Substring(0, exclamIndex);\n                        string ident = prefix.Substring(exclamIndex + 1, atIndex - (exclamIndex + 1));\n                        string host = prefix.Substring(atIndex + 1);\n                        connectionManager.OnUserJoinedChannel(channel, host, userName, ident);\n                        break;\n                    case \"PART\":\n                        string pChannel = parameters[0];\n                        string pUserName = prefix.Substring(0, prefix.IndexOf('!'));\n                        connectionManager.OnUserLeftChannel(pChannel, pUserName);\n                        break;\n                    case \"QUIT\":\n                        string qUserName = prefix.Substring(0, prefix.IndexOf('!'));\n                        connectionManager.OnUserQuitIRC(qUserName);\n                        break;\n                    case \"PRIVMSG\":\n                        if (parameters.Count > 1 && Convert.ToInt32(parameters[1][0]) == 1 && !parameters[1].Contains(\"ACTION\"))\n                        {\n                            goto case \"NOTICE\";\n                        }\n                        string pmsgUserName = prefix.Substring(0, prefix.IndexOf('!'));\n                        string pmsgIdent = GetIdentFromPrefix(prefix);\n                        string[] recipients = new string[parameters.Count - 1];\n                        for (int pid = 0; pid < parameters.Count - 1; pid++)\n                            recipients[pid] = parameters[pid];\n                        string privmsg = parameters[parameters.Count - 1];\n                        if (parameters[1].StartsWith('\\u0001'.ToString() + \"ACTION\"))\n                            privmsg = privmsg.Substring(1).Remove(privmsg.Length - 2);\n                        foreach (string recipient in recipients)\n                        {\n                            if (recipient.StartsWith(\"#\"))\n                                connectionManager.OnChatMessageReceived(recipient, pmsgUserName, pmsgIdent, privmsg);\n                            else if (recipient == ProgramConstants.PLAYERNAME)\n                                connectionManager.OnPrivateMessageReceived(pmsgUserName, privmsg);\n                            //else if (pmsgUserName == ProgramConstants.PLAYERNAME)\n                            //{\n                            //    DoPrivateMessageSent(privmsg, recipient);\n                            //}\n                        }\n                        break;\n                    case \"MODE\":\n                        string modeUserName = prefix.Substring(0, prefix.IndexOf('!'));\n                        string modeChannelName = parameters[0];\n                        string modeString = parameters[1];\n                        List<string> modeParameters =\n                            parameters.Count > 2 ? parameters.GetRange(2, parameters.Count - 2) : new List<string>();\n                        connectionManager.OnChannelModesChanged(modeUserName, modeChannelName, modeString, modeParameters);\n                        break;\n                    case \"KICK\":\n                        string kickChannelName = parameters[0];\n                        string kickUserName = parameters[1];\n                        connectionManager.OnUserKicked(kickChannelName, kickUserName);\n                        break;\n                    case \"ERROR\":\n                        connectionManager.OnErrorReceived(message);\n                        break;\n                    case \"PING\":\n                        if (parameters.Count > 0)\n                        {\n                            QueueMessage(new QueuedMessage(\"PONG \" + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000));\n                            Logger.Log(\"PONG \" + parameters[0]);\n                        }\n                        else\n                        {\n                            QueueMessage(new QueuedMessage(\"PONG\", QueuedMessageType.SYSTEM_MESSAGE, 5000));\n                            Logger.Log(\"PONG\");\n                        }\n                        break;\n                    case \"TOPIC\":\n                        if (parameters.Count < 2)\n                            break;\n\n                        connectionManager.OnChannelTopicChanged(prefix.Substring(0, prefix.IndexOf('!')),\n                            parameters[0], parameters[1]);\n                        break;\n                    case \"NICK\":\n                        int nickExclamIndex = prefix.IndexOf('!');\n                        if (nickExclamIndex > -1 || parameters.Count < 1)\n                        {\n                            string oldNick = prefix.Substring(0, nickExclamIndex);\n                            string newNick = parameters[0];\n                            Logger.Log(\"Nick change - \" + oldNick + \" -> \" + newNick);\n                            connectionManager.OnUserNicknameChange(oldNick, newNick);\n                        }\n                        break;\n                }\n            }\n            catch\n            {\n                Logger.Log(\"Warning: Failed to parse command \" + message);\n            }\n        }\n\n        private string GetIdentFromPrefix(string prefix)\n        {\n            int atIndex = prefix.IndexOf('@');\n            int exclamIndex = prefix.IndexOf('!');\n\n            if (exclamIndex == -1 || atIndex == -1)\n                return string.Empty;\n\n            return prefix.Substring(exclamIndex + 1, atIndex - (exclamIndex + 1));\n        }\n\n        /// <summary>\n        /// Parses a single IRC message received from the server.\n        /// </summary>\n        /// <param name=\"message\">The message.</param>\n        /// <param name=\"prefix\">(out) The message prefix.</param>\n        /// <param name=\"command\">(out) The command.</param>\n        /// <param name=\"parameters\">(out) The parameters of the command.</param>\n        private void ParseIrcMessage(string message, out string prefix, out string command, out List<string> parameters)\n        {\n            int prefixEnd = -1;\n            prefix = command = String.Empty;\n            parameters = new List<string>();\n\n            // Grab the prefix if it is present. If a message begins\n            // with a colon, the characters following the colon until\n            // the first space are the prefix.\n            if (message.StartsWith(\":\"))\n            {\n                prefixEnd = message.IndexOf(\" \");\n                prefix = message.Substring(1, prefixEnd - 1);\n            }\n\n            // Grab the trailing if it is present. If a message contains\n            // a space immediately following a colon, all characters after\n            // the colon are the trailing part.\n            int trailingStart = message.IndexOf(\" :\");\n            string trailing = null;\n            if (trailingStart >= 0)\n                trailing = message.Substring(trailingStart + 2);\n            else\n                trailingStart = message.Length;\n\n            // Use the prefix end position and trailing part start\n            // position to extract the command and parameters.\n            var commandAndParameters = message.Substring(prefixEnd + 1, trailingStart - prefixEnd - 1).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);\n\n            if (commandAndParameters.Length == 0)\n            {\n                command = String.Empty;\n                Logger.Log(\"Nonexistant command!\");\n                return;\n            }\n\n            // The command will always be the first element of the array.\n            command = commandAndParameters[0];\n\n            // The rest of the elements are the parameters, if they exist.\n            // Skip the first element because that is the command.\n            if (commandAndParameters.Length > 1)\n            {\n                for (int id = 1; id < commandAndParameters.Length; id++)\n                {\n                    parameters.Add(commandAndParameters[id]);\n                }\n            }\n\n            // If the trailing part is valid add the trailing part to the\n            // end of the parameters.\n            if (!string.IsNullOrEmpty(trailing))\n                parameters.Add(trailing);\n        }\n\n        #endregion\n\n        #region Sending commands\n\n        private void RunSendQueue()\n        {\n            while (_isConnected)\n            {\n                string message = String.Empty;\n\n                lock (messageQueueLocker)\n                {\n                    for (int i = 0; i < MessageQueue.Count; i++)\n                    {\n                        QueuedMessage qm = MessageQueue[i];\n                        if (qm.Delay > 0)\n                        {\n                            if (qm.SendAt < DateTime.Now)\n                            {\n                                message = qm.Command;\n\n                                Logger.Log(\"Delayed message sent: \" + qm.ID);\n\n                                MessageQueue.RemoveAt(i);\n                                break;\n                            }\n                        }\n                        else\n                        {\n                            message = qm.Command;\n                            MessageQueue.RemoveAt(i);\n                            break;\n                        }\n                    }\n                }\n\n                if (String.IsNullOrEmpty(message))\n                {\n                    Thread.Sleep(10);\n                    continue;\n                }\n\n                SendMessage(message);\n\n                Thread.Sleep(MessageQueueDelay);\n            }\n\n            lock (messageQueueLocker)\n            {\n                MessageQueue.Clear();\n            }\n\n            sendQueueExited = true;\n        }\n\n        /// <summary>\n        /// Sends a PING message to the server to indicate that we're still connected.\n        /// </summary>\n        /// <param name=\"data\">Just a dummy parameter so that this matches the delegate System.Threading.TimerCallback.</param>\n        private void AutoPing(object data)\n        {\n            SendMessage(\"PING LAG\" + Rng.Next(100000, 999999));\n        }\n\n        /// <summary>\n        /// Registers the user.\n        /// </summary>\n        private void Register()\n        {\n            if (welcomeMessageReceived)\n                return;\n\n            Logger.Log(\"Registering.\");\n\n            var defaultGame = ClientConfiguration.Instance.LocalGame;\n\n            string realname = ProgramConstants.GAME_VERSION + \" \" + defaultGame + \" CnCNet\";\n\n            SendMessage(string.Format(\"USER {0} 0 * :{1}\", defaultGame + \".\" +\n                systemId, realname));\n\n            SendMessage(\"NICK \" + ProgramConstants.PLAYERNAME);\n        }\n\n        public void ChangeNickname()\n        {\n            SendMessage(\"NICK \" + ProgramConstants.PLAYERNAME);\n        }\n\n        public void QueueMessage(QueuedMessageType type, int priority, string message, bool replace = false)\n        {\n            QueuedMessage qm = new QueuedMessage(message, type, priority, replace);\n            QueueMessage(qm);\n        }\n\n        public void QueueMessage(QueuedMessageType type, int priority, int delay, string message)\n        {\n            QueuedMessage qm = new QueuedMessage(message, type, priority, delay);\n            QueueMessage(qm);\n            Logger.Log(\"Setting delay to \" + delay + \"ms for \" + qm.ID);\n        }\n\n        /// <summary>\n        /// Send a message to the CnCNet server.\n        /// </summary>\n        /// <param name=\"message\">The message to send.</param>\n        private void SendMessage(string message)\n        {\n            if (serverStream == null)\n                return;\n\n            Logger.Log(\"SRM: \" + message);\n\n            byte[] buffer = encoding.GetBytes(message + \"\\r\\n\");\n            if (serverStream.CanWrite)\n            {\n                try\n                {\n                    serverStream.Write(buffer, 0, buffer.Length);\n                    serverStream.Flush();\n                }\n                catch (IOException ex)\n                {\n                    Logger.Log(\"Sending message to the server failed! Reason: \" + ex.ToString());\n                }\n            }\n        }\n\n        private int NextQueueID { get; set; } = 0;\n\n        /// <summary>\n        /// This will attempt to replace a previously queued message of the same type.\n        /// </summary>\n        /// <param name=\"qm\">The new message to replace with</param>\n        /// <returns>Whether or not a replace occurred</returns>\n        private bool ReplaceMessage(QueuedMessage qm)\n        {\n            lock (messageQueueLocker)\n            {\n                var previousMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType);\n                if (previousMessageIndex == -1)\n                    return false;\n\n                MessageQueue[previousMessageIndex] = qm;\n                return true;\n            }\n        }\n\n        /// <summary>\n        /// Adds a message to the send queue.\n        /// </summary>\n        /// <param name=\"qm\">The message to queue.</param>\n        /// <param name=\"replace\">If true, attempt to replace a previous message of the same type</param>\n        public void QueueMessage(QueuedMessage qm)\n        {\n            if (!_isConnected)\n                return;\n\n            if (qm.Replace && ReplaceMessage(qm))\n                return;\n\n            qm.ID = NextQueueID++;\n\n            lock (messageQueueLocker)\n            {\n                switch (qm.MessageType)\n                {\n                    case QueuedMessageType.GAME_BROADCASTING_MESSAGE:\n                    case QueuedMessageType.GAME_PLAYERS_MESSAGE:\n                    case QueuedMessageType.GAME_SETTINGS_MESSAGE:\n                    case QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE:\n                    case QueuedMessageType.GAME_LOCKED_MESSAGE:\n                    case QueuedMessageType.GAME_GET_READY_MESSAGE:\n                    case QueuedMessageType.GAME_NOTIFICATION_MESSAGE:\n                    case QueuedMessageType.GAME_HOSTING_MESSAGE:\n                    case QueuedMessageType.WHOIS_MESSAGE:\n                    case QueuedMessageType.GAME_CHEATER_MESSAGE:\n                        AddSpecialQueuedMessage(qm);\n                        break;\n                    case QueuedMessageType.INSTANT_MESSAGE:\n                        SendMessage(qm.Command);\n                        break;\n                    default:\n                        int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority);\n                        if (ProgramConstants.LOG_LEVEL > 1)\n                            Logger.Log(\"QM Undefined: \" + qm.Command + \" \" + placeInQueue);\n                        if (placeInQueue == -1)\n                            MessageQueue.Add(qm);\n                        else\n                            MessageQueue.Insert(placeInQueue, qm);\n                        break;\n                }\n            }\n        }\n\n        /// <summary>\n        /// Adds a \"special\" message to the send queue that replaces\n        /// previous messages of the same type in the queue.\n        /// </summary>\n        /// <param name=\"qm\">The message to queue.</param>\n        private void AddSpecialQueuedMessage(QueuedMessage qm)\n        {\n            int broadcastingMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType);\n\n            qm.ID = NextQueueID++;\n\n            if (broadcastingMessageIndex > -1)\n            {\n                if (ProgramConstants.LOG_LEVEL > 1)\n                    Logger.Log(\"QM Replace: \" + qm.Command + \" \" + broadcastingMessageIndex);\n                MessageQueue[broadcastingMessageIndex] = qm;\n            }\n            else\n            {\n                int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority);\n                if (ProgramConstants.LOG_LEVEL > 1)\n                    Logger.Log(\"QM: \" + qm.Command + \" \" + placeInQueue);\n                if (placeInQueue == -1)\n                    MessageQueue.Add(qm);\n                else\n                    MessageQueue.Insert(placeInQueue, qm);\n            }\n        }\n\n        #endregion\n    }\n}"
  },
  {
    "path": "DXMainClient/Online/EventArguments/AttemptedServerEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    /// <summary>\n    /// Event arguments for a server connection attempt.\n    /// </summary>\n    public class AttemptedServerEventArgs : EventArgs\n    {\n        public AttemptedServerEventArgs(string serverName)\n        {\n            ServerName = serverName;\n        }\n\n        public string ServerName { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/CTCPEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class CTCPEventArgs : EventArgs\n    {\n        public CTCPEventArgs(string sender, string channelName, string ctcpMessage)\n        {\n            Sender = sender;\n            ChannelName = channelName;\n            CTCPMessage = ctcpMessage;\n        }\n\n        public string Sender { get; private set; }\n        public string ChannelName { get; private set; }\n        public string CTCPMessage { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ChannelCTCPEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class ChannelCTCPEventArgs : EventArgs\n    {\n        public ChannelCTCPEventArgs(string userName, string message)\n        {\n            UserName = userName;\n            Message = message;\n        }\n\n        public string UserName { get; private set; }\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ChannelEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class ChannelEventArgs : EventArgs\n    {\n        public ChannelEventArgs(string channelName)\n        {\n            ChannelName = channelName;\n        }\n\n        public string ChannelName { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ChannelModeEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class ChannelModeEventArgs : EventArgs\n    {\n        public ChannelModeEventArgs(string userName, string modeString)\n        {\n            UserName = userName;\n            ModeString = modeString;\n        }\n\n        public string UserName { get; set; }\n        public string ModeString { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ChannelTopicEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class ChannelTopicEventArgs : EventArgs\n    {\n        public ChannelTopicEventArgs(string channelName, string topic)\n        {\n            ChannelName = channelName;\n            Topic = topic;\n        }\n\n        public string ChannelName { get; private set; }\n        public string Topic { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/CnCNetPrivateMessageEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class CnCNetPrivateMessageEventArgs : EventArgs\n    {\n        public CnCNetPrivateMessageEventArgs(string sender, string message)\n        {\n            Sender = sender;\n            Message = message;\n            DateTime = DateTime.Now;\n        }\n        \n        public DateTime DateTime { get; set; }\n\n        public string Sender { get; private set; }\n\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ConnectionLostEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class ConnectionLostEventArgs : EventArgs\n    {\n        public ConnectionLostEventArgs(string reason)\n        {\n            Reason = reason;\n        }\n        public string Reason { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/FavoriteMapEventArgs.cs",
    "content": "﻿using System;\nusing DTAClient.Domain.Multiplayer;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class FavoriteMapEventArgs : EventArgs\n    {\n        public readonly Map Map;\n\n        public FavoriteMapEventArgs(Map map)\n        {\n            Map = map;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/GameOptionPresetEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class GameOptionPresetEventArgs : EventArgs\n    {\n        public string PresetName { get; }\n\n        public GameOptionPresetEventArgs(string presetName)\n        {\n            PresetName = presetName;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/JoinUserEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class JoinUserEventArgs : EventArgs\n    {\n        public IRCUser IrcUser { get; }\n\n        public JoinUserEventArgs(IRCUser ircUser)\n        {\n            IrcUser = ircUser;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/KickEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class KickEventArgs : EventArgs\n    {\n        public KickEventArgs(string channelName, string userName)\n        {\n            ChannelName = channelName;\n            UserName = userName;\n        }\n\n        public string ChannelName { get; private set; }\n        public string UserName { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/MultiplayerNameRightClickedEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class MultiplayerNameRightClickedEventArgs : EventArgs\n    {\n        public string PlayerName { get; }\n\n        public MultiplayerNameRightClickedEventArgs(string playerName)\n        {\n            PlayerName = playerName;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/PrivateCTCPEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class PrivateCTCPEventArgs : EventArgs\n    {\n        public PrivateCTCPEventArgs(string sender, string message)\n        {\n            Sender = sender;\n            Message = message;\n        }\n\n        public string Sender { get; private set; }\n\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/PrivateMessageEventArgs.cs",
    "content": "﻿namespace DTAClient.Online.EventArguments\n{\n    public class PrivateMessageEventArgs : CnCNetPrivateMessageEventArgs\n    {\n        public readonly IRCUser ircUser;\n\n        public PrivateMessageEventArgs(string sender, string message, IRCUser ircUser) : base(sender, message)\n        {\n            this.ircUser = ircUser;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/ServerMessageEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    /// <summary>\n    /// Generic event argument class for a IRC server message.\n    /// </summary>\n    public class ServerMessageEventArgs : EventArgs\n    {\n        public ServerMessageEventArgs(string message)\n        {\n            Message = message;\n        }\n\n        public string Message { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/UnreadMessageCountEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class UnreadMessageCountEventArgs : EventArgs\n    {\n        public int UnreadMessageCount { get; set; }\n\n        public UnreadMessageCountEventArgs(int unreadMessageCount)\n        {\n            UnreadMessageCount = unreadMessageCount;\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/UserAwayEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class UserAwayEventArgs : EventArgs\n    {\n        public UserAwayEventArgs(string user, string awayReason)\n        {\n            UserName = user;\n            AwayReason = awayReason;\n        }\n\n        public string UserName { get; private set; }\n\n        public string AwayReason { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/UserListEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class UserListEventArgs : EventArgs\n    {\n        public UserListEventArgs(string channelName, string[] userNames)\n        {\n            ChannelName = channelName;\n            UserNames = userNames;\n        }\n\n        public string ChannelName { get; private set; }\n        public string[] UserNames { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/EventArguments/WhoEventArgs.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online.EventArguments\n{\n    public class WhoEventArgs : EventArgs\n    {\n        public WhoEventArgs(string ident, string userName, string extraInfo)\n        {\n            Ident = ident;\n            UserName = userName;\n            ExtraInfo = extraInfo;\n        }\n        \n        public string Ident { get; private set; }\n\n        public string UserName { get; private set; }\n        public string ExtraInfo { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/FileHashCalculator.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text;\n\nusing ClientCore;\nusing ClientCore.I18N;\nusing ClientCore.Enums;\n\nusing Rampastring.Tools;\n\nnamespace DTAClient.Online\n{\n    public class FileHashCalculator\n    {\n        private const string CONFIGNAME = \"FHCConfig.ini\";\n        private bool calculateGameExeHash = true;\n\n        private static readonly IReadOnlyList<string> knownTextFileExtensions = [\".txt\", \".ini\", \".json\", \".xml\"];\n\n        private string[] fileNamesToCheck = ClientConfiguration.Instance.ClientGameType switch\n        {\n            ClientType.TS => new string[]\n            {\n                \"spawner.xdp\",\n                \"rules.ini\",\n                \"ai.ini\",\n                \"art.ini\",\n                \"shroud.shp\",\n                \"INI/Rules.ini\",\n                \"INI/Enhance.ini\",\n                \"INI/Firestrm.ini\",\n                \"INI/Art.ini\",\n                \"INI/ArtE.ini\",\n                \"INI/ArtFS.ini\",\n                \"INI/AI.ini\",\n                \"INI/AIE.ini\",\n                \"INI/AIFS.ini\"\n            },\n            ClientType.YR => new string[]\n            {\n                \"spawner.xdp\",\n                \"spawner2.xdp\",\n                \"artmd.ini\",\n                \"soundmd.ini\",\n                \"aimd.ini\",\n                \"shroud.shp\",\n                \"INI/Map Code/Cooperative.ini\",\n                \"INI/Map Code/Free For All.ini\",\n                \"INI/Map Code/Land Rush.ini\",\n                \"INI/Map Code/Meat Grinder.ini\",\n                \"INI/Map Code/Megawealth.ini\",\n                \"INI/Map Code/Naval War.ini\",\n                \"INI/Map Code/Standard.ini\",\n                \"INI/Map Code/Team Alliance.ini\",\n                \"INI/Map Code/Unholy Alliance.ini\",\n                \"INI/Game Options/Allies Allowed.ini\",\n                \"INI/Game Options/Brutal AI.ini\",\n                \"INI/Game Options/No Dog Engi Eat.ini\",\n                \"INI/Game Options/No Spawn Previews.ini\",\n                \"INI/Game Options/RA2 Classic Mode.ini\",\n                \"INI/Map Code/GlobalCode.ini\",\n                \"INI/Map Code/MultiplayerGlobalCode.ini\"\n            },\n            ClientType.Ares => new string[]\n            {\n                \"Ares.dll\",\n                \"Ares.dll.inj\",\n                \"Ares.mix\",\n                \"Syringe.exe\",\n                \"cncnet5.dll\",\n                \"rulesmd.ini\",\n                \"artmd.ini\",\n                \"soundmd.ini\",\n                \"aimd.ini\",\n                \"shroud.shp\"\n            },\n            _ => new string[] { }\n        };\n\n        public FileHashCalculator() => ParseConfigFile();\n\n        private string finalHash = string.Empty;\n\n        public void CalculateHashes()\n        {\n            FileHashes fh = new()\n            {\n                ClientDefinitionsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ClientConfiguration.CLIENT_DEFS)),\n                GameOptionsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, ClientConfiguration.GAME_OPTIONS)),\n                ClientDXHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"clientdx.exe\")),\n                ClientXNAHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"clientxna.exe\")),\n                ClientOGLHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"clientogl.exe\")),\n                ClientDXNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"BinariesNET8\", \"Windows\", \"clientdx.dll\")),\n                ClientXNANET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"BinariesNET8\", \"XNA\", \"clientxna.dll\")),\n                ClientOGLNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"BinariesNET8\", \"OpenGL\", \"clientogl.dll\")),\n                ClientUGLNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), \"BinariesNET8\", \"UniversalGL\", \"clientogl.dll\")),\n                GameExeHash = calculateGameExeHash\n                    ? CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.GetGameExecutableName()))\n                    : string.Empty,\n                LauncherExeHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.GameLauncherExecutableName)),\n                MPMapsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)),\n                FHCConfigHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.BASE_RESOURCE_PATH, CONFIGNAME)),\n            };\n\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\{ClientConfiguration.CLIENT_DEFS}: {fh.ClientDefinitionsHash}\");\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\{CONFIGNAME}: {fh.FHCConfigHash}\");\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\{ClientConfiguration.GAME_OPTIONS}: {fh.GameOptionsHash}\");\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\clientdx.exe: {fh.ClientDXHash}\");\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\clientxna.exe: {fh.ClientXNAHash}\");\n            Logger.Log($\"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\\\clientogl.exe: {fh.ClientOGLHash}\");\n            Logger.Log($\"Hash for ClientDX NET8: {fh.ClientDXNET8Hash}\");\n            Logger.Log($\"Hash for ClientXNA NET8: {fh.ClientXNANET8Hash}\");\n            Logger.Log($\"Hash for ClientOGL NET8: {fh.ClientOGLNET8Hash}\");\n            Logger.Log($\"Hash for ClientUGL NET8: {fh.ClientUGLNET8Hash}\");\n            Logger.Log($\"Hash for {ClientConfiguration.Instance.MPMapsIniPath}: {fh.MPMapsHash}\");\n\n            if (calculateGameExeHash)\n                Logger.Log($\"Hash for {ClientConfiguration.Instance.GetGameExecutableName()}: {fh.GameExeHash}\");\n\n            if (!string.IsNullOrEmpty(ClientConfiguration.Instance.GameLauncherExecutableName))\n                Logger.Log($\"Hash for {ClientConfiguration.Instance.GameLauncherExecutableName}: {fh.LauncherExeHash}\");\n\n            foreach (string relativePath in fileNamesToCheck)\n            {\n                string fullPath = SafePath.CombineFilePath(ProgramConstants.GamePath, relativePath);\n                string hash = fh.AddHashForFileIfExists(relativePath, fullPath);\n                if (!string.IsNullOrEmpty(hash))\n                    Logger.Log($\"Hash for {relativePath}: {hash}\");\n            }\n\n            List<DirectoryInfo> iniPaths = [SafePath.GetDirectory(ProgramConstants.GamePath, \"INI\", \"Game Options\")];\n\n            if (ClientConfiguration.Instance.ClientGameType != ClientType.YR)\n                iniPaths.Add(SafePath.GetDirectory(ProgramConstants.GamePath, \"INI\", \"Map Code\"));\n\n            foreach (DirectoryInfo path in iniPaths)\n            {\n                if (path.Exists)\n                {\n                    foreach (string filename in path.EnumerateFiles(\"*\", SearchOption.AllDirectories).Select(s => s.FullName.Substring(path.FullName.Length)))\n                    {\n                        string fileRelativePath = SafePath.CombineFilePath(path.Name, filename);\n                        string fileFullPath = SafePath.CombineFilePath(path.FullName, filename);\n                        Debug.Assert(File.Exists(fileFullPath), $\"File {fileFullPath} is supposed to but does not exist.\");\n\n                        string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath);\n                        if (!string.IsNullOrEmpty(hash))\n                            Logger.Log(\"Hash for \" + fileRelativePath + \": \" + hash);\n                    }\n                }\n            }\n\n            // Add the hashes for each checked file from the available translations\n\n            if (Directory.Exists(ClientConfiguration.Instance.TranslationsFolderPath))\n            {\n                DirectoryInfo translationsFolderPath = SafePath.GetDirectory(ClientConfiguration.Instance.TranslationsFolderPath);\n\n                List<TranslationGameFile> translationGameFiles = ClientConfiguration.Instance.TranslationGameFiles\n                    .Where(tgf => tgf.Checked).ToList();\n\n                foreach (DirectoryInfo translationFolder in translationsFolderPath.EnumerateDirectories())\n                {\n                    foreach (TranslationGameFile tgf in translationGameFiles)\n                    {\n                        string fileRelativePath = SafePath.CombineFilePath(translationFolder.Name, tgf.Source);\n                        string fileFullPath = SafePath.CombineFilePath(translationFolder.FullName, tgf.Source);\n\n                        string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath);\n                        if (!string.IsNullOrEmpty(hash))\n                            Logger.Log($\"Hash for {fileRelativePath}: {hash}\");\n                    }\n                }\n            }\n\n            finalHash = fh.GetFinalHash();\n            Logger.Log($\"Complete hash: {finalHash}\");\n        }\n\n        public string GetCompleteHash() => finalHash;\n\n        private void ParseConfigFile()\n        {\n            IniFile config = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), CONFIGNAME));\n            calculateGameExeHash = config.GetBooleanValue(\"Settings\", \"CalculateGameExeHash\", true);\n\n            List<string> keys = config.GetSectionKeys(\"FilenameList\");\n            if (keys == null || keys.Count < 1)\n                return;\n\n            List<string> filenames = new List<string>();\n            foreach (string key in keys)\n            {\n                string value = config.GetStringValue(\"FilenameList\", key, string.Empty);\n                filenames.Add(value == string.Empty ? key : value);\n            }\n\n            fileNamesToCheck = filenames.ToArray();\n        }\n\n        private static string NormalizePath(string path) => path.Replace('\\\\', '/');\n\n        private static string CalculateSHA1ForFile(string path)\n        {\n            if (string.IsNullOrWhiteSpace(path))\n                return string.Empty;\n\n            FileInfo file = SafePath.GetFile(path);\n            if (!file.Exists)\n                return string.Empty;\n\n            using Stream inputStream = file.OpenRead();\n\n            if (knownTextFileExtensions.Contains(file.Extension, StringComparer.InvariantCultureIgnoreCase))\n            {\n                // Normalize line endings to LF\n                UTF8Encoding utf8Encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false);\n\n                using StreamReader reader = new(inputStream, utf8Encoding, detectEncodingFromByteOrderMarks: false);\n                string text = reader.ReadToEnd();\n                text = text.Replace(\"\\r\\n\", \"\\n\").Trim();\n\n                byte[] bytes = utf8Encoding.GetBytes(text);\n\n                using SHA1 sha1 = SHA1.Create();\n                return BytesToString(sha1.ComputeHash(bytes));\n            }\n            else\n            {\n                using SHA1 sha1 = SHA1.Create();\n                return BytesToString(sha1.ComputeHash(inputStream));\n            }\n        }\n\n        private static string BytesToString(byte[] bytes)\n        {\n            char[] result = new char[bytes.Length * 2];\n            for (int i = 0; i < bytes.Length; i++)\n            {\n                byte b = bytes[i];\n                result[i * 2] = GetHexChar(b >> 4);\n                result[i * 2 + 1] = GetHexChar(b & 0x0F);\n            }\n            return new string(result);\n        }\n\n        private static char GetHexChar(int digit)\n        {\n            Debug.Assert(digit >= 0 && digit < 16, $\"Value {digit} is out of range for a hex digit.\");\n            return (char)(digit < 10 ? '0' + digit : 'a' + digit - 10);\n        }\n\n        private class FileHashes()\n        {\n            public string ClientDefinitionsHash;\n            public string GameOptionsHash;\n            public string ClientDXHash;\n            public string ClientXNAHash;\n            public string ClientOGLHash;\n            public string ClientDXNET8Hash;\n            public string ClientXNANET8Hash;\n            public string ClientOGLNET8Hash;\n            public string ClientUGLNET8Hash;\n            public string MPMapsHash;\n            public string GameExeHash;\n            public string LauncherExeHash;\n            public string FHCConfigHash;\n\n            public readonly SortedDictionary<string, string> AdditionalFileHashes = new(StringComparer.InvariantCultureIgnoreCase);\n\n            public string AddHashForFileIfExists(string relativePath) =>\n                AddHashForFileIfExists(relativePath, relativePath);\n\n            public string AddHashForFileIfExists(string relativePath, string filePath)\n            {\n                Debug.Assert(!relativePath.StartsWith(ProgramConstants.GamePath), $\"File path {relativePath} should be a relative path.\");\n\n                string hash = CalculateSHA1ForFile(filePath);\n                if (!string.IsNullOrEmpty(hash))\n                {\n                    AdditionalFileHashes[NormalizePath(relativePath)] = hash;\n                    return hash;\n                }\n                else\n                {\n                    return string.Empty;\n                }\n            }\n\n            public string GetFinalHash()\n            {\n                var sb = new StringBuilder();\n                sb.Append(ClientDefinitionsHash);\n                sb.Append(GameOptionsHash);\n                sb.Append(ClientDXHash);\n                sb.Append(ClientXNAHash);\n                sb.Append(ClientOGLHash);\n                sb.Append(ClientDXNET8Hash);\n                sb.Append(ClientXNANET8Hash);\n                sb.Append(ClientOGLNET8Hash);\n                sb.Append(ClientUGLNET8Hash);\n                sb.Append(GameExeHash);\n                sb.Append(LauncherExeHash);\n                sb.Append(MPMapsHash);\n                sb.Append(FHCConfigHash);\n\n                // Append additional file hashes, ordered by key\n                foreach (string fileHash in AdditionalFileHashes.Values)\n                    sb.Append(fileHash);\n\n                // Merge hashes\n                string finalHash = sb.ToString();\n                byte[] buffer = Encoding.ASCII.GetBytes(finalHash);\n                using SHA1 sha1 = SHA1.Create();\n                byte[] hash = sha1.ComputeHash(buffer);\n                return BytesToString(hash);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/IConnectionManager.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// An interface for handling IRC messages.\n    /// </summary>\n    public interface IConnectionManager\n    {\n        void OnWelcomeMessageReceived(string message);\n\n        void OnGenericServerMessageReceived(string message);\n\n        void OnAwayMessageReceived(string userName, string reason);\n\n        void OnChannelTopicReceived(string channelName, string topic);\n\n        void OnChannelTopicChanged(string userName, string channelName, string topic);\n\n        void OnUserListReceived(string channelName, string[] userList);\n\n        void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo);\n\n        void OnChannelFull(string channelName);\n\n        void OnTargetChangeTooFast(string channelName, string message);\n\n        void OnChannelInviteOnly(string channelName);\n\n        void OnIncorrectChannelPassword(string channelName);\n\n        void OnCTCPParsed(string channelName, string userName, string message);\n\n        void OnNoticeMessageParsed(string notice, string userName);\n\n        void OnUserJoinedChannel(string channelName, string hostName, string userName, string ident);\n\n        void OnUserLeftChannel(string channelName, string userName);\n\n        void OnUserQuitIRC(string userName);\n\n        void OnChatMessageReceived(string receiver, string senderName, string senderIdent, string message);\n\n        void OnPrivateMessageReceived(string sender, string message);\n\n        void OnChannelModesChanged(string userName, string channelName, string modeString, List<string> modeParameters);\n\n        void OnUserKicked(string channelName, string userName);\n\n        void OnErrorReceived(string errorMessage);\n\n        void OnNameAlreadyInUse();\n\n        void OnBannedFromChannel(string channelName);\n\n        void OnUserNicknameChange(string oldNickname, string newNickname);\n\n        // **********************\n        // Connection-related methods\n        // **********************\n\n        void OnAttemptedServerChanged(string serverName);\n\n        void OnConnectAttemptFailed();\n\n        void OnConnectionLost(string reason);\n\n        void OnReconnectAttempt();\n\n        void OnDisconnected();\n\n        void OnConnected();\n\n        bool GetDisconnectStatus();\n\n        void OnServerLatencyTested(int candidateCount, int closerCount);\n\n        //public EventHandler<ServerMessageEventArgs> WelcomeMessageReceived;\n        //public EventHandler<ServerMessageEventArgs> GenericServerMessageReceived;\n        //public EventHandler<UserAwayEventArgs> AwayMessageReceived;\n        //public EventHandler<ChannelTopicEventArgs> ChannelTopicReceived;\n        //public EventHandler<UserListEventArgs> UserListReceived;\n        //public EventHandler<WhoEventArgs> WhoReplyReceived;\n        //public EventHandler<ChannelEventArgs> ChannelFull;\n        //public EventHandler<ChannelEventArgs> IncorrectChannelPassword;\n\n        //public event EventHandler<AttemptedServerEventArgs> AttemptedServerChanged;\n        //public event EventHandler ConnectAttemptFailed;\n        //public event EventHandler<ConnectionLostEventArgs> ConnectionLost;\n        //public event EventHandler ReconnectAttempt;\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/IRCColor.cs",
    "content": "﻿using Microsoft.Xna.Framework;\n\nnamespace DTAClient.Online\n{\n    public class IRCColor\n    {\n        public IRCColor(string name, bool selectable, Color xnaColor, int ircColorId)\n        {\n            Name = name;\n            Selectable = selectable;\n            XnaColor = xnaColor;\n            IrcColorId = ircColorId;\n        }\n\n        public string Name { get; private set; }\n        public bool Selectable { get; private set; }\n        public Color XnaColor { get; private set; }\n        public int IrcColorId { get; private set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/IRCUser.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// A user on an IRC server.\n    /// </summary>\n    public class IRCUser : ICloneable\n    {\n        public IRCUser() { }\n\n        public IRCUser(string name)\n        {\n            Name = name;\n        }\n\n        public IRCUser(string name, string ident, string host)\n        {\n            Name = name;\n            Ident = ident;\n            Hostname = host;\n        }\n\n        public string Name { get; set; }\n        public string Ident { get; set; }\n        public string Hostname { get; set; }\n        public int GameID { get; set; } = -1;\n\n        public List<string> Channels = new List<string>();\n\n        public object Clone()\n        {\n            return MemberwiseClone();\n        }\n\n        public bool IsFriend { get; set; }\n        public bool IsIgnored { get; set; }\n        public bool HasVoice { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/IUserCollection.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    public interface IUserCollection<T>\n    {\n        int Count { get; }\n\n        void Add(string username, T item);\n        void Clear();\n        void DoForAllUsers(Action<T> action);\n        T Find(string username);\n        LinkedListNode<T> GetFirst();\n        void Reinsert(string username);\n        bool Remove(string username);\n    }\n}"
  },
  {
    "path": "DXMainClient/Online/PrivateMessageHandler.cs",
    "content": "﻿using System;\nusing DTAClient.Online.EventArguments;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// This is responsible for handling the receiving of private messages from CnCNet and performing any logic checks\n    /// as to whether the message should be ignored, independent from any GUI. This will then forward valid private message\n    /// events to other consumers.\n    /// </summary>\n    public class PrivateMessageHandler\n    {\n        private readonly CnCNetUserData _cncnetUserData;\n        private readonly CnCNetManager _connectionManager;\n        \n        private int UnreadMessageCount;\n        \n        public event EventHandler<PrivateMessageEventArgs> PrivateMessageReceived;\n        public event EventHandler<UnreadMessageCountEventArgs> UnreadMessageCountUpdated;\n\n        public PrivateMessageHandler(\n            CnCNetManager connectionManager,\n            CnCNetUserData cncnetUserData\n        )\n        {\n            _connectionManager = connectionManager;\n            _cncnetUserData = cncnetUserData;\n\n            _connectionManager.PrivateMessageReceived += _PrivateMessageReceived;\n        }\n\n        private void _PrivateMessageReceived(object sender, CnCNetPrivateMessageEventArgs e)\n        {\n            IRCUser iu = _connectionManager.UserList.Find(u => u.Name == e.Sender);\n\n            // We don't accept PMs from people who we don't share any channels with\n            if (iu == null)\n                return;\n\n            // Messages from users we've blocked are not wanted\n            if (_cncnetUserData.IsIgnored(iu.Ident))\n                return;\n\n            var privateMessageEventArgs = new PrivateMessageEventArgs(e.Sender, e.Message, iu);\n\n            PrivateMessageReceived?.Invoke(this, privateMessageEventArgs);\n        }\n\n        private void DoUnreadMessageCountUpdated() \n            => UnreadMessageCountUpdated?.Invoke(this, new UnreadMessageCountEventArgs(UnreadMessageCount));\n\n        private void SetUnreadMessageCount(int unreadMessageCount)\n        {\n            UnreadMessageCount = unreadMessageCount;\n            DoUnreadMessageCountUpdated();\n        }\n\n        /// <summary>\n        /// This can be called by specific GUI components to trigger than any unread counts should be reset,\n        /// because the PrivateMessageWindow was made visible.\n        /// </summary>\n        public void ResetUnreadMessageCount() \n            => SetUnreadMessageCount(0);\n\n        /// <summary>\n        /// This can be called by specific GUI components to trigger than any unread counts should be incremented,\n        /// because the PrivateMessageWindow may not currently be visible.\n        /// </summary>\n        public void IncrementUnreadMessageCount() \n            => SetUnreadMessageCount(UnreadMessageCount + 1);\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/PrivateMessageUser.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    class PrivateMessageUser\n    {\n        public PrivateMessageUser(IRCUser user)\n        {\n            IrcUser = user;\n        }\n\n        public IRCUser IrcUser { get; private set; }\n\n        public List<ChatMessage> Messages = new List<ChatMessage>();\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/QueuedMessage.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// A queued network message.\n    /// </summary>\n    public class QueuedMessage\n    {\n        private const int DEFAULT_DELAY = -1;\n        private const int REPLACE_DELAY = 1;\n        \n        public QueuedMessage(string command, QueuedMessageType type, int priority) : \n            this(command, type, priority, DEFAULT_DELAY, false)\n        {\n        }\n\n        public QueuedMessage(string command, QueuedMessageType type, int priority, bool replace) : \n            this(command, type, priority, replace ? REPLACE_DELAY : DEFAULT_DELAY, replace)\n        {\n        }\n\n        public QueuedMessage(string command, QueuedMessageType type, int priority, int delay) :\n            this(command, type, priority, delay, false)\n        {\n        }\n\n        private QueuedMessage(string command, QueuedMessageType type, int priority, int delay, bool replace)\n        {\n            Command = command;\n            MessageType = type;\n            Priority = priority;\n            Delay = delay;\n            SendAt = Delay < 0  ? DateTime.Now : DateTime.Now.AddMilliseconds(Delay);\n            Replace = replace;\n        }\n\n        /// <summary>\n        /// Message Queue ID\n        /// </summary>\n        public int ID { get; set; }\n\n        /// <summary>\n        /// The command to send to the IRC network.\n        /// </summary>\n        public string Command { get; set; }\n\n        /// <summary>\n        /// The type of the message.\n        /// </summary>\n        public QueuedMessageType MessageType { get; set; }\n\n        /// <summary>\n        /// The priority of the message.\n        /// </summary>\n        public int Priority { get; set; }\n\n        /// <summary>\n        /// The amount of milliseconds to delay the message.\n        /// </summary>\n        public int Delay { get; set; }\n\n        /// <summary>\n        /// The amount of milliseconds to delay the message.\n        /// </summary>\n        public DateTime SendAt { get; set; }\n\n        /// <summary>\n        /// This can be used to replace a message on the queue to help prevent flooding purposes.\n        /// This should be used with at least a small delay.\n        /// </summary>\n        public bool Replace { get; set; } = false;\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/QueuedMessageType.cs",
    "content": "﻿namespace DTAClient.Online\n{\n    /// <summary>\n    /// The type of a CnCNet IRC network message.\n    /// </summary>\n    public enum QueuedMessageType\n    {\n        UNDEFINED,\n        CHAT_MESSAGE,\n        SYSTEM_MESSAGE,\n        GAME_SETTINGS_MESSAGE,\n        GAME_PLAYERS_MESSAGE,\n        GAME_PLAYERS_READY_STATUS_MESSAGE,\n        GAME_LOCKED_MESSAGE,\n        GAME_GET_READY_MESSAGE,\n        GAME_NOTIFICATION_MESSAGE,\n        GAME_HOSTING_MESSAGE,\n        GAME_CHEATER_MESSAGE,\n        GAME_BROADCASTING_MESSAGE,\n        WHOIS_MESSAGE,\n        INSTANT_MESSAGE,\n        GAME_PLAYERS_EXTRA_MESSAGE,\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/RecentPlayer.cs",
    "content": "﻿using System;\nusing System.Text.Json.Serialization;\n\nnamespace DTAClient.Online\n{\n    public class RecentPlayer\n    {\n        [JsonInclude]\n        public string PlayerName { get; set; }\n\n        [JsonInclude]\n        public string GameName { get; set; }\n\n        [JsonInclude]\n        public DateTime GameTime { get; set; }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/Server.cs",
    "content": "﻿using System;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// A struct containing information on an IRC server.\n    /// </summary>\n    public struct Server\n    {\n        public Server(string host, string name, int[] ports)\n        {\n            Host = host;\n            Name = name;\n            Ports = ports;\n        }\n\n        public string Host;\n        public string Name;\n        public int[] Ports;\n\n        public string Serialize() => FormattableString.Invariant($\"{Host}|{Name}|{string.Join(\",\", Ports)}\");\n\n        public static Server Deserialize(string serialized)\n        {\n            string[] parts = serialized.Split('|');\n            string host = parts[0];\n            string name = parts[1];\n            string[] portStrings = parts[2].Split(',');\n            int[] ports = new int[portStrings.Length];\n\n            for (int i = 0; i < portStrings.Length; i++)\n                ports[i] = int.Parse(portStrings[i]);\n\n            return new Server(host, name, ports);\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/SortedUserCollection.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// A custom collection that aims to provide quick insertion,\n    /// removal and lookup operations while always keeping the list sorted\n    /// by combining Dictionary and LinkedList.\n    /// </summary>\n    public class SortedUserCollection<T> : IUserCollection<T>\n    {\n        public SortedUserCollection(Func<T, T, int> userComparer)\n        {\n            dictionary = new Dictionary<string, LinkedListNode<T>>();\n            linkedList = new LinkedList<T>();\n            this.userComparer = userComparer;\n        }\n\n        private readonly Dictionary<string, LinkedListNode<T>> dictionary;\n        private readonly LinkedList<T> linkedList;\n\n        private readonly Func<T, T, int> userComparer;\n\n        public int Count => dictionary.Count;\n\n        public void Add(string username, T item)\n        {\n            if (linkedList.Count == 0)\n            {\n                var node = linkedList.AddFirst(item);\n                dictionary.Add(username.ToLower(), node);\n                return;\n            }\n\n            var currentNode = linkedList.First;\n            while (true)\n            {\n                if (userComparer(currentNode.Value, item) > 0)\n                {\n                    var node = linkedList.AddBefore(currentNode, item);\n                    dictionary.Add(username.ToLower(), node);\n                    break;\n                }\n\n                if (currentNode.Next == null)\n                {\n                    var node = linkedList.AddAfter(currentNode, item);\n                    dictionary.Add(username.ToLower(), node);\n                    break;\n                }\n\n                currentNode = currentNode.Next;\n            }\n        }\n\n        public bool Remove(string username)\n        {\n            if (dictionary.TryGetValue(username.ToLower(), out var node))\n            {\n                linkedList.Remove(node);\n                dictionary.Remove(username.ToLower());\n                return true;\n            }\n\n            return false;\n        }\n\n        public T Find(string username)\n        {\n            if (dictionary.TryGetValue(username.ToLower(), out var node))\n                return node.Value;\n\n            return default(T);\n        }\n\n        public void Reinsert(string username)\n        {\n            var existing = Find(username.ToLower());\n            if (existing == null)\n                return;\n\n            Remove(username);\n            Add(username, existing);\n        }\n\n        public void Clear()\n        {\n            linkedList.Clear();\n            dictionary.Clear();\n        }\n\n        public LinkedListNode<T> GetFirst() => linkedList.First;\n\n        public void DoForAllUsers(Action<T> action)\n        {\n            var current = linkedList.First;\n            while (current != null)\n            {\n                action(current.Value);\n                current = current.Next;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/Online/UnsortedUserCollection.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DTAClient.Online\n{\n    /// <summary>\n    /// A custom collection that aims to provide quick insertion,\n    /// removal and lookup operations by using a dictionary. Does not\n    /// keep the list sorted.\n    /// </summary>\n    public class UnsortedUserCollection<T> : IUserCollection<T>\n    {\n        private Dictionary<string, T> dictionary = new Dictionary<string, T>();\n\n        public int Count => dictionary.Count;\n\n        public void Add(string username, T item)\n        {\n            dictionary.Add(username.ToLower(), item);\n        }\n\n        public void Clear()\n        {\n            dictionary.Clear();\n        }\n\n        public void DoForAllUsers(Action<T> action)\n        {\n            var values = dictionary.Values;\n            \n            foreach (T value in values)\n            {\n                action(value);\n            }\n        }\n\n        public T Find(string username)\n        {\n            if (dictionary.TryGetValue(username.ToLower(), out T value))\n                return value;\n\n            return default(T);\n        }\n\n        LinkedListNode<T> IUserCollection<T>.GetFirst()\n        {\n            throw new NotImplementedException();\n        }\n\n        void IUserCollection<T>.Reinsert(string username)\n        {\n            throw new NotImplementedException();\n        }\n\n        public bool Remove(string username)\n        {\n            return dictionary.Remove(username.ToLower());\n        }\n    }\n}\n"
  },
  {
    "path": "DXMainClient/PreStartup.cs",
    "content": "﻿using System;\n#if WINFORMS\nusing System.Windows.Forms;\n#endif\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\nusing DTAClient.Domain;\nusing Rampastring.Tools;\nusing ClientCore;\nusing System.Security.AccessControl;\nusing System.Security.Principal;\nusing System.Collections.Generic;\nusing ClientCore.Extensions;\nusing System.Linq;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Versioning;\nusing ClientCore.I18N;\nusing System.Globalization;\nusing System.Security;\nusing System.Transactions;\nusing DTAClient.DXGUI.Multiplayer.GameLobby;\n\nnamespace DTAClient\n{\n    /// <summary>\n    /// Contains client startup parameters.\n    /// </summary>\n    struct StartupParams\n    {\n        public StartupParams(bool noAudio, bool multipleInstanceMode,\n            List<string> unknownParams)\n        {\n            NoAudio = noAudio;\n            MultipleInstanceMode = multipleInstanceMode;\n            UnknownStartupParams = unknownParams;\n        }\n\n        public bool NoAudio { get; }\n        public bool MultipleInstanceMode { get; }\n        public List<string> UnknownStartupParams { get; }\n    }\n\n    static class PreStartup\n    {\n        private static readonly Stopwatch startupStopwatch = Stopwatch.StartNew();\n        public static TimeSpan StartupElapsed => startupStopwatch.Elapsed;\n\n        /// <summary>\n        /// Initializes various basic systems like the client's logger, \n        /// constants, and the general exception handler.\n        /// Reads the user's settings from an INI file, \n        /// checks for necessary permissions and starts the client if\n        /// everything goes as it should.\n        /// </summary>\n        /// <param name=\"parameters\">The client's startup parameters.</param>\n        public static void Initialize(StartupParams parameters)\n        {\n            Translation.InitialUICulture = CultureInfo.CurrentUICulture;\n            CultureInfo.CurrentUICulture = new CultureInfo(ProgramConstants.HARDCODED_LOCALE_CODE);\n\n#if WINFORMS\n            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);\n            Application.ThreadException += (sender, args) => HandleException(sender, args.Exception);\n#endif\n            AppDomain.CurrentDomain.UnhandledException += (sender, args) => HandleException(sender, (Exception)args.ExceptionObject);\n\n            DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath);\n\n            Environment.CurrentDirectory = gameDirectory.FullName;\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                CheckPermissions();\n\n            DirectoryInfo clientUserFilesDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath);\n            FileInfo clientLogFile = SafePath.GetFile(clientUserFilesDirectory.FullName, \"client.log\");\n            ProgramConstants.LogFileName = clientLogFile.FullName;\n\n            if (clientLogFile.Exists)\n            {\n                // Copy client.log file as client_previous.log. Override client_previous.log if it exists.\n                FileInfo clientPrevLogFile = SafePath.GetFile(clientUserFilesDirectory.FullName, \"client_previous.log\");\n                if (clientPrevLogFile.Exists)\n                    File.Delete(clientPrevLogFile.FullName);\n                File.Move(clientLogFile.FullName, clientPrevLogFile.FullName);\n            }\n\n            Logger.Initialize(clientUserFilesDirectory.FullName, clientLogFile.Name);\n            Logger.WriteLogFile = true;\n            MainClientConstants.LoggerInitialized = true;\n\n            if (!clientUserFilesDirectory.Exists)\n                clientUserFilesDirectory.Create();\n\n            Logger.Log(\"***Logfile for \" + MainClientConstants.GAME_NAME_LONG + \" client***\");\n\n            string clientVersion = GitVersionInformation.AssemblySemVer;\n#if DEVELOPMENT_BUILD\n            clientVersion = $\"{GitVersionInformation.CommitDate} {GitVersionInformation.BranchName}@{GitVersionInformation.ShortSha}\";\n#endif\n\n            Logger.Log($\"Client version: {clientVersion}\");\n            Logger.Log(GitVersionInformation.InformationalVersion);\n\n#if DEVELOPMENT_BUILD\n            Logger.Log(\"This is a development build of the client. Stability and reliability may not be fully guaranteed.\");\n#endif\n            MainClientConstants.Initialize();\n\n            // Log information about given startup params\n            if (parameters.NoAudio)\n            {\n                Logger.Log(\"Startup parameter: No audio\");\n\n                // TODO fix\n                throw new NotImplementedException(\"-NOAUDIO is currently not implemented, please run the client without it.\".L10N(\"Client:Main:NoAudio\"));\n            }\n\n            if (parameters.MultipleInstanceMode)\n                Logger.Log(\"Startup parameter: Allow multiple client instances\");\n\n            parameters.UnknownStartupParams.ForEach(p => Logger.Log(\"Unknown startup parameter: \" + p));\n\n            Logger.Log(\"Loading settings.\");\n\n            UserINISettings.Initialize(ClientConfiguration.Instance.SettingsIniName);\n\n            // Try to load translation\n            try\n            {\n                Translation translation;\n                FileInfo translationThemeFile = SafePath.GetFile(UserINISettings.Instance.TranslationThemeFolderPath, ClientConfiguration.Instance.TranslationIniName);\n                FileInfo translationFile = SafePath.GetFile(UserINISettings.Instance.TranslationFolderPath, ClientConfiguration.Instance.TranslationIniName);\n\n                if (translationFile.Exists)\n                {\n                    Logger.Log($\"Loading generic translation file at {translationFile.FullName}\");\n                    translation = new Translation(translationFile.FullName, UserINISettings.Instance.Translation);\n                    if (translationThemeFile.Exists)\n                    {\n                        Logger.Log($\"Loading theme-specific translation file at {translationThemeFile.FullName}\");\n                        translation.AppendValuesFromIniFile(translationThemeFile.FullName);\n                    }\n\n                    Translation.Instance = translation;\n                }\n                else\n                {\n                    Logger.Log($\"Failed to load a translation file. \" +\n                        $\"Neither {translationThemeFile.FullName} nor {translationFile.FullName} exist.\");\n                }\n\n                Logger.Log(\"Loaded translation: \" + Translation.Instance.Name);\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Failed to load the translation file. \" + ex.ToString());\n                Translation.Instance = new Translation(UserINISettings.Instance.Translation);\n            }\n\n            CultureInfo.CurrentUICulture = Translation.Instance.Culture;\n\n            try\n            {\n                if (UserINISettings.Instance.GenerateTranslationStub)\n                {\n                    string stubPath = SafePath.CombineFilePath(\n                        ProgramConstants.ClientUserFilesPath, ClientConfiguration.Instance.TranslationIniName);\n\n                    AppDomain.CurrentDomain.ProcessExit += (sender, e) =>\n                    {\n                        Logger.Log(\"Writing the translation stub file.\");\n                        var ini = Translation.Instance.DumpIni(UserINISettings.Instance.GenerateOnlyNewValuesInTranslationStub);\n                        ini.WriteIniFile(stubPath);\n                    };\n\n                    Logger.Log(\"Translation stub generation feature is now enabled. The stub file will be written when the client exits.\");\n\n                    // Lookup all compile-time available strings\n                    ClientCore.Generated.TranslationNotifier.Register();\n                    ClientGUI.Generated.TranslationNotifier.Register();\n                    ClientUpdater.Generated.TranslationNotifier.Register();\n                    DTAClient.Generated.TranslationNotifier.Register();\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"Failed to generate the translation stub: \" + ex.ToString());\n            }\n\n            // Custom mission initialization\n            CustomMissionHelper.Initialize();\n            CustomMissionHelper.DeleteSupplementalMissionFiles();\n\n            // Delete obsolete files from old target project versions\n            Task.Run(() =>\n            {\n                gameDirectory.EnumerateFiles(\"mainclient.log\").SingleOrDefault()?.Delete();\n                gameDirectory.EnumerateFiles(\"aunchupdt.dat\").SingleOrDefault()?.Delete();\n\n                try\n                {\n                    gameDirectory.EnumerateFiles(\"wsock32.dll\").SingleOrDefault()?.Delete();\n                }\n                catch (Exception ex)\n                {\n                    LogException(ex);\n\n                    string error = (\"Deleting wsock32.dll failed! Please close any \" +\n                        \"applications that could be using the file, and then start the client again.\" + \"\\n\\n\" +\n                        \"Message:\").L10N(\"Client:Main:DeleteWsock32Failed\") + \" \" + ex.Message;\n\n                    MainClientConstants.DisplayErrorAction(null, error, true);\n                }\n            });\n\n            Startup startup = new();\n#if DEBUG\n            startup.Execute();\n#else\n            try\n            {\n                startup.Execute();\n            }\n            catch (Exception ex)\n            {\n                // MainClientConstants.DisplayErrorAction might have been overriden by XNA messagebox, which might be unable to display an error message.\n                // Fallback to MessageBox.\n                MainClientConstants.DisplayErrorAction = MainClientConstants.DefaultDisplayErrorAction;\n                HandleException(startup, ex);\n            }\n#endif\n\n        }\n\n        public static void LogException(Exception ex, bool innerException = false)\n        {\n            if (!innerException)\n                Logger.Log(\"KABOOOOOOM!!! Info:\");\n            else\n                Logger.Log(\"InnerException info:\");\n\n            Logger.Log(\"Type: \" + ex.GetType());\n            Logger.Log(\"Message: \" + ex.Message);\n            Logger.Log(\"Source: \" + ex.Source);\n            Logger.Log(\"TargetSite.Name: \" + ex.TargetSite?.Name);\n            Logger.Log(\"Stacktrace: \" + ex.StackTrace);\n\n            if (ex.InnerException is not null)\n                LogException(ex.InnerException, true);\n        }\n\n        public static void HandleException(object sender, Exception ex)\n        {\n            LogException(ex, innerException: false);\n\n            string errorLogPath = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, \"ClientCrashLogs\", FormattableString.Invariant($\"ClientCrashLog{DateTime.Now.ToString(\"_yyyy_MM_dd_HH_mm\")}.txt\"));\n            bool crashLogCopied = false;\n\n            try\n            {\n                DirectoryInfo crashLogsDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, \"ClientCrashLogs\");\n\n                if (!crashLogsDirectoryInfo.Exists)\n                    crashLogsDirectoryInfo.Create();\n\n                File.Copy(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, \"client.log\"), errorLogPath, true);\n                crashLogCopied = true;\n            }\n            catch { }\n\n            string error = string.Format(\"{0} has crashed. Error message:\".L10N(\"Client:Main:FatalErrorText1\") + Environment.NewLine + Environment.NewLine +\n                ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ?\n                \"A crash log has been saved to the following file:\".L10N(\"Client:Main:FatalErrorText2\") + \" \" + Environment.NewLine + Environment.NewLine +\n                errorLogPath + Environment.NewLine + Environment.NewLine : \"\") +\n                (crashLogCopied ? \"If the issue is repeatable, contact the {1} staff at {2} and provide the crash log file.\".L10N(\"Client:Main:FatalErrorText3\") :\n                \"If the issue is repeatable, contact the {1} staff at {2}.\".L10N(\"Client:Main:FatalErrorText4\")),\n                MainClientConstants.GAME_NAME_LONG,\n                MainClientConstants.GAME_NAME_SHORT,\n                MainClientConstants.SUPPORT_URL_SHORT);\n\n            MainClientConstants.DisplayErrorAction(\"KABOOOOOOOM\".L10N(\"Client:Main:FatalErrorTitle\"), error, true);\n        }\n\n        [SupportedOSPlatform(\"windows\")]\n        private static void CheckPermissions()\n        {\n            if (UserHasDirectoryAccessRights(ProgramConstants.GamePath, FileSystemRights.Modify))\n                return;\n\n            string error = string.Format((\"You seem to be running {0} from a write-protected directory.\\n\\n\" +\n                \"For {1} to function properly when run from a write-protected directory, it needs administrative privileges.\\n\\n\" +\n                \"Please also make sure that your security software isn't blocking {1}.\").L10N(\"Client:Main:AdminRequiredExplanation\"),\n                MainClientConstants.GAME_NAME_LONG, MainClientConstants.GAME_NAME_SHORT);\n\n            string question = \"Would you like to restart the client with administrative rights?\".L10N(\"Client:Main:AdminRequiredRestartPrompt\");\n\n            string title = \"Administrative privileges required\".L10N(\"Client:Main:AdminRequiredTitle\");\n\n#if WINFORMS && NETFRAMEWORK\n            DialogResult result = MessageBox.Show(error + \"\\n\\n\" + question, title, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);\n            if (result == DialogResult.Yes)\n            {\n                AdminRestarter.RestartAsAdmin();\n            }\n#else\n            MainClientConstants.DisplayErrorAction(title, error, true);\n#endif\n            Environment.Exit(1);\n        }\n\n        /// <summary>\n        /// Checks whether the client has specific file system rights to a directory.\n        /// See ssds's answer at https://stackoverflow.com/questions/1410127/c-sharp-test-if-user-has-write-access-to-a-folder\n        /// </summary>\n        /// <param name=\"path\">The path to the directory.</param>\n        /// <param name=\"accessRights\">The file system rights.</param>\n        [SupportedOSPlatform(\"windows\")]\n        private static bool UserHasDirectoryAccessRights(string path, FileSystemRights accessRights)\n        {\n            var currentUser = WindowsIdentity.GetCurrent();\n            var principal = new WindowsPrincipal(currentUser);\n\n            // If the user is not running the client with administrator privileges in Program Files, they need to be prompted to do so.\n            if (!principal.IsInRole(WindowsBuiltInRole.Administrator))\n            {\n                string progfiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);\n                string progfilesx86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);\n                if (ProgramConstants.GamePath.Contains(progfiles) || ProgramConstants.GamePath.Contains(progfilesx86))\n                    return false;\n            }\n\n            var isInRoleWithAccess = false;\n\n            try\n            {\n                var di = new DirectoryInfo(path);\n                var acl = di.GetAccessControl();\n                var rules = acl.GetAccessRules(true, true, typeof(NTAccount));\n\n                foreach (AuthorizationRule rule in rules)\n                {\n                    var fsAccessRule = rule as FileSystemAccessRule;\n                    if (fsAccessRule == null)\n                        continue;\n\n                    if ((fsAccessRule.FileSystemRights & accessRights) > 0)\n                    {\n                        var ntAccount = rule.IdentityReference as NTAccount;\n                        if (ntAccount == null)\n                            continue;\n\n                        try\n                        {\n                            if (principal.IsInRole(ntAccount.Value))\n                            {\n                                if (fsAccessRule.AccessControlType == AccessControlType.Deny)\n                                    return false;\n                                isInRoleWithAccess = true;\n                            }\n                        }\n                        catch (SecurityException)\n                        {\n                            //IsInRole may throw for selected roles when running in Wine, keep iterating other rules \n                            continue;\n                        }\n                    }\n                }\n            }\n            catch (UnauthorizedAccessException)\n            {\n                return false;\n            }\n            return isInRoleWithAccess;\n        }\n    }\n}"
  },
  {
    "path": "DXMainClient/Program.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Versioning;\nusing System.Text;\n\n#if !NETFRAMEWORK\nusing System.Runtime.Loader;\n#endif\nusing System.Threading;\n\n/* !! We cannot use references to other projects or non-framework assemblies in this class, assembly loading events not hooked up yet !! */\n\nnamespace DTAClient\n{\n    static class Program\n    {\n        static Program()\n        {\n            /* We have different binaries depending on build platform, but for simplicity\n             * the target projects (DTA, TI, MO, YR) supply them all in a single download.\n             * To avoid DLL hell, we load the binaries from different directories\n             * depending on the build platform. */\n\n            DirectoryInfo currentDir = new FileInfo(Assembly.GetEntryAssembly().Location).Directory;\n            string startupPath = SearchResourcesDir(currentDir.FullName);\n\n            string binariesFolderName = \"Binaries\";\n#if !NETFRAMEWORK\n            binariesFolderName = \"BinariesNET8\";\n#endif\n\n            COMMON_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName) + Path.DirectorySeparatorChar;\n\n#if XNA\n            SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, \"XNA\") + Path.DirectorySeparatorChar;\n#elif GL && ISWINDOWS\n            SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, \"OpenGL\") + Path.DirectorySeparatorChar;\n#elif GL && !ISWINDOWS\n            SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, \"UniversalGL\") + Path.DirectorySeparatorChar;\n#elif DX\n            SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, \"Windows\") + Path.DirectorySeparatorChar;\n#else\n#error Yuri has won\n#endif\n\n#if !DEBUG\n#if !NETFRAMEWORK\n            // Set up DLL load paths as early as possible\n            AssemblyLoadContext.Default.Resolving += DefaultAssemblyLoadContextOnResolving;\n#else\n            // Set up DLL load paths as early as possible\n            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;\n#endif\n#endif\n        }\n\n        private static string COMMON_LIBRARY_PATH;\n        private static string SPECIFIC_LIBRARY_PATH;\n\n        static void InitializeApplicationConfiguration()\n        {\n#if WINFORMS\n\n#if NET6_0_OR_GREATER\n            // .NET 6.0 brings a source generator ApplicationConfiguration which is not available in previous .NET versions\n            // https://medium.com/c-sharp-progarmming/whats-new-in-windows-forms-in-net-6-0-840c71856751\n            ApplicationConfiguration.Initialize();\n#else\n\n#if NETCOREAPP3_0_OR_GREATER\n#if GL\n            System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware);\n#else\n            System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.PerMonitorV2);\n#endif\n#endif\n\n            System.Windows.Forms.Application.EnableVisualStyles();\n            System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);\n#endif\n\n#else\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                SetProcessDPIAware();\n#endif\n        }\n\n        [DllImport(\"user32.dll\")]\n        [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]\n        [SupportedOSPlatform(\"windows\")]\n        private static extern bool SetProcessDPIAware();\n\n        /// <summary>\n        /// The main entry point for the application.\n        /// </summary>\n#if WINFORMS\n        [STAThread]\n#endif\n        static void Main(string[] args)\n        {\n            // https://stackoverflow.com/questions/3967716/how-to-find-encoding-for-1251-codepage\n            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);\n\n            InitializeApplicationConfiguration();\n\n            bool noAudio = false;\n            bool multipleInstanceMode = false;\n            List<string> unknownStartupParams = new List<string>();\n\n            for (int arg = 0; arg < args.Length; arg++)\n            {\n                string argument = args[arg].ToUpperInvariant();\n\n                switch (argument)\n                {\n                    case \"-NOAUDIO\":\n                        noAudio = true;\n                        break;\n                    case \"-MULTIPLEINSTANCE\":\n                        multipleInstanceMode = true;\n                        break;\n                    default:\n                        unknownStartupParams.Add(argument);\n                        break;\n                }\n            }\n\n            var parameters = new StartupParams(noAudio, multipleInstanceMode, unknownStartupParams);\n\n            if (multipleInstanceMode)\n            {\n                // Proceed to client startup\n                PreStartup.Initialize(parameters);\n                return;\n            }\n\n            // We're a single instance application!\n            // http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c/229567\n            // Global prefix means that the mutex is global to the machine\n            string mutexId = FormattableString.Invariant($\"Global{Guid.Parse(\"1CC9F8E7-9F69-4BBC-B045-E734204027A9\")}\");\n            using var mutex = new Mutex(false, mutexId, out _);\n            bool hasHandle = false;\n\n            try\n            {\n                try\n                {\n                    hasHandle = mutex.WaitOne(8000, false);\n                    if (hasHandle == false)\n                        throw new TimeoutException(\"Timeout waiting for exclusive access\");\n                }\n                catch (AbandonedMutexException)\n                {\n                    hasHandle = true;\n                }\n                catch (TimeoutException)\n                {\n                    return;\n                }\n\n                // Proceed to client startup\n                PreStartup.Initialize(parameters);\n            }\n            finally\n            {\n                if (hasHandle)\n                    mutex.ReleaseMutex();\n            }\n        }\n\n#if !NETFRAMEWORK\n        private static Assembly DefaultAssemblyLoadContextOnResolving(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)\n        {\n            if (assemblyName.Name.EndsWith(\".resources\", StringComparison.OrdinalIgnoreCase))\n                return null;\n\n            // the specific dll should be in priority than the common one\n\n            var specificFileInfo = new FileInfo(Path.Combine(SPECIFIC_LIBRARY_PATH, FormattableString.Invariant($\"{assemblyName.Name}.dll\")));\n\n            if (specificFileInfo.Exists)\n                return assemblyLoadContext.LoadFromAssemblyPath(specificFileInfo.FullName);\n\n            var commonFileInfo = new FileInfo(Path.Combine(COMMON_LIBRARY_PATH, FormattableString.Invariant($\"{assemblyName.Name}.dll\")));\n\n            if (commonFileInfo.Exists)\n                return assemblyLoadContext.LoadFromAssemblyPath(commonFileInfo.FullName);\n\n            return null;\n        }\n#else\n        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)\n        {\n            string unresolvedAssemblyName = args.Name.Split(',').First();\n\n            if (unresolvedAssemblyName.EndsWith(\".resources\", StringComparison.OrdinalIgnoreCase))\n                return null;\n\n            // the specific dll should be in priority than the common one\n\n            var specificFileInfo = new FileInfo(FormattableString.Invariant($\"{Path.Combine(SPECIFIC_LIBRARY_PATH, unresolvedAssemblyName)}.dll\"));\n\n            if (specificFileInfo.Exists)\n                return Assembly.Load(AssemblyName.GetAssemblyName(specificFileInfo.FullName));\n\n            var commonFileInfo = new FileInfo(FormattableString.Invariant($\"{Path.Combine(COMMON_LIBRARY_PATH, unresolvedAssemblyName)}.dll\"));\n\n            if (commonFileInfo.Exists)\n                return Assembly.Load(AssemblyName.GetAssemblyName(commonFileInfo.FullName));\n\n            return null;\n        }\n#endif\n\n        /// <summary>\n        /// This method finds the \"Resources\" directory by traversing the directory tree upwards from the startup path.\n        /// </summary>\n        /// <remarks>\n        /// This method is needed by both ClientCore and DXMainClient. However, since it is usually called at the very beginning,\n        /// where DXMainClient could not refer to ClientCore, this method is copied to both projects.\n        /// Remember to keep <see cref=\"ClientCore.ProgramConstants.SearchResourcesDir\"/> and <see cref=\"DTAClient.Program.SearchResourcesDir\"/> consistent if you have modified its source codes.\n        /// </remarks>\n        private static string SearchResourcesDir(string startupPath)\n        {\n            DirectoryInfo currentDir = new(startupPath);\n            for (int i = 0; i < 3; i++)\n            {\n                // Determine if currentDir is the \"Resources\" folder\n                if (currentDir.Name.ToLowerInvariant() == \"Resources\".ToLowerInvariant())\n                    return currentDir.FullName;\n\n                // Additional check. This makes developers to debug the client inside Visual Studio a little bit easier.\n                DirectoryInfo resourcesDir = currentDir.GetDirectories(\"Resources\", SearchOption.TopDirectoryOnly).FirstOrDefault();\n                if (resourcesDir is not null)\n                    return resourcesDir.FullName;\n\n                currentDir = currentDir.Parent;\n            }\n\n            throw new Exception(\"Could not find Resources directory.\");\n        }\n\n    }\n}"
  },
  {
    "path": "DXMainClient/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"DXMainClient\": {\n      \"commandName\": \"Project\"\n    },\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"distributionName\": \"\"\n    }\n  }\n}"
  },
  {
    "path": "DXMainClient/Resources/ClientDefinitions.ini",
    "content": "; Dawn of the Tiberium Age CnCNet Client Definitions\n\n[Themes]\n0=Default theme,Default Theme\\\n\n[Settings]\n; Set type of the game used with client. Allowed options: TS, YR, Ares\nClientGameType=TS\n\n; The executable that is started by the client after creating the .dxfail\n; file if DirectX11 initialization fails\nLauncherExe=TiberianSun.exe\n\n; The filename of the game executable to launch\n; Accepts multiple entries separated with a comma,\n; but currently only the first entry is ever used\nGameExecutableNames=game.exe\n\n; Game executable name for Linux/Mac systems\nUnixGameExecutableName=wine-ts.sh\n\n; List of executables to check for DirectDraw compatibility mode issues\n; Comma-separated list of executables in the execution chain\n;CompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe\n\n; The main map file extension that is read by the client (e.g. map for TS/RA2, ini for Dune)\nMapFileExtension=map\n\nExtraCommandLineParams=-CD.\n\n; The filename of the client and game settings configuration file\nSettingsFile=SUN.ini\n\n; Path to the file that defines multiplayer maps\nMPMapsPath=INI\\MPMaps.ini\n\n; Game modes that custom maps are allowed to show up in\nAllowedCustomGameModes=Custom Map,Standard\n\n; The number of loading screens for each side in the game\nLoadingScreenCount=2\n\n; The local game, corresponding to game definitions in ClientCore\nLocalGame=TS\n\n; Long game name, displayed in various places in the UI\nLongGameName=Tiberian Sun\n\n; The name that's displayed as the window's title\nWindowTitle=Tiberian Sun Client\n\n; Install path in registry, from HKEY_CURRENT_USER\\Software\\\nRegistryInstallPath=TiberianSun\n\n; CnCNet 5 live status identifier\nCnCNetLiveStatusIdentifier=cncnet5_ts\n\n; The URL to open when the user clicks on \"Credits\" in the Extras menu\nCreditsURL=https://downloads.cncnet.org/updates/games/ts/live/credits.htm\n\n; The URL to open when the user clicks on the change log link in the update query window\nChangelogURL=https://downloads.cncnet.org/updates/games/ts/live/changelog.txt\n\n; The support URL displayed when the client crashes or fails to update\nLongSupportURL=http://ppmforums.com/index.php?f=24\n\n; The shortened version of the above-mentioned support URL\nShortSupportURL=ppmforums.com/index.php?f=24\n\n; File to launch when the user wants to open the map editor\nMapEditorExePath=Map Editor\\Map Editor.bat\n\n; Path to FinalSun map editor configuration file, the client writes its install path to the file\nFSIniPath=Map Editor\\FinalSun.ini\n\n; File name of BattleFS.ini equivalent, in the INI\\ directory\n; It's recommended to just write all campaigns in Battle.ini instead\nBattleFSFileName=BattleFS.ini\n\n; Whether to write SidebarHack=true to spawn.ini on game launch\n; Enable or disable this if GDI and Nod are unintentionally using each other's sidebars.\nSidebarHack=yes\n\n; The minimum width that the client window is rendered at,\n; smaller resolutions are downscaled\nMinimumRenderWidth=1024\n\n; The minimum height that the client window is rendered at,\n; smaller resolutions are downscaled\nMinimumRenderHeight=600\n\n; The maximum width that the client window is rendered at,\n; larger resolutions are upscaled\nMaximumRenderWidth=1280\n\n; The maximum height that the client window is rendered at,\n; larger resolutions are upscaled\nMaximumRenderHeight=720\n\n; Resolutions that are given the \"(Recommended)\" suffix in the options window\nRecommendedResolutions=1280x720\t;,1360x768,1366x768,2560x1440\n\n; Hotkey configuration file for the hotkey configuration window\n; in the options menu\nKeyboardINI=Keyboard.ini\n\n; Hotkey section in KeyboardINI, for RA this should be WinHotkeys\nKeyboardHotkeySection=Hotkey\n\n; The name of the log file to parse statistics from at the end of the game\nStatisticsLogFileName=TS.LOG\n\n; Whether or not previously used game options in skirmish are saved across client sessions\nSaveSkirmishGameOptions=yes\n\n; Whether or not previously used game options in campaign are saved across client sessions\nSaveCampaignGameOptions=yes\n\n; Discord application ID for Discord integration\nDiscordAppId=1041056809349304441\n\n; The filename of the QuickMatch executable to launch\nQuickmatch=CnCNetQM.exe\n\n; Set to true to disable the updater and to hide the \"cheater!\" dialog when modding the game\nModMode=true\n\n; Activates warnings for non-release build of XNA Client. \n; Please, make sure you are not publishing stable mod version with unstable development client build. \nShowDevelopmentBuildWarnings=true"
  },
  {
    "path": "DXMainClient/Resources/DTA/CampaignSelector.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[CampaignSelector]\nBackgroundTexture=MainMenu/dbak.png\nDrawMode=Centered\nDrawBorders=false\nSize=672,600\n\n[lbCampaignList]\nSize=322,554\nEnableScrollbar=true\n\n[lblMissionDescriptionHeader]\nTextAnchor=RIGHT\nAnchorPoint=346,12\n\n[tbMissionDescription]\nLocation=346,34\nDistanceFromRightBorder=12\n\n[lblDifficultyLevel]\nTextAnchor=HORIZONTAL_CENTER\nAnchorPoint=503,476\n\n[trbDifficultySelector]\nLocation=346,498\nDistanceFromRightBorder=12\n\n[lblEasy]\nTextAnchor=RIGHT\nAnchorPoint=346,534\n\n[lblNormal]\nTextAnchor=HORIZONTAL_CENTER\nAnchorPoint=503,534\n\n[lblHard]\nTextAnchor=LEFT\nAnchorPoint=660,534\n\n[lblDifficultyNames]\nLocation=346,538\n\n[btnLaunch]\nIdleTexture=147pxbtn.png\nHoverTexture=147pxbtn_c.png\nWidth=147\nDistanceFromRightBorder=179\nDistanceFromBottomBorder=12\n\n[btnCancel]\nIdleTexture=147pxbtn.png\nHoverTexture=147pxbtn_c.png\nWidth=147\nDistanceFromRightBorder=12\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/CheaterScreen.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[lblCheater]\nText=Modifications Detected!\nLocation=97,12\n\n[lblDescription]\nText=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?\n\n[imagePanel]\nBackgroundTexture=cheater.png\nLocation=27,124\nSize=280,280\t;310,x"
  },
  {
    "path": "DXMainClient/Resources/DTA/CnCNetGameLobby.ini",
    "content": "[INISystem]\nBasedOn=MultiplayerGameLobby.ini\n\n[MultiplayerGameLobby]\n$CCMP99=btnChangeTunnel:XNAClientButton\n$CCMP100=btnGameLobbySettings:XNAClientButton\n\n[btnChangeTunnel]\n$Width=133\n$X=getX(btnLeaveGame) - getWidth($Self) - BUTTON_SPACING\n$Y=getY(btnLaunchGame)\nText=Change Tunnel\n\n[btnGameLobbySettings]\n$Width=133\n$X=getX(btnChangeTunnel) - getWidth($Self) - BUTTON_SPACING\n$Y=getY(btnLaunchGame)\nText=Lobby Settings\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/CnCNetLobby.ini",
    "content": "[INISystem]\nBasedOn=LANLobby.ini\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/aqrit.cfg",
    "content": ";;; www.bitpatch.com ;;;\n\nRealDDrawPath            = AUTO\nBltMirror                = 0\nBltNoTearing             = 0\nColorFix                 = 0\nDisableHighDpiScaling    = 0\nFakeVsync                = 0\nFakeVsyncInterval        = 0\nForceBltNoTearing        = 0\nForceDirectDrawEmulation = 1\nNoVideoMemory            = 0\nSingleProcAffinity       = 0\nShowFPS                  = 0\n\n \n\n\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/cnc-ddraw.ini",
    "content": "; cnc-ddraw - https://github.com/CnCNet/cnc-ddraw - https://cncnet.org\n\n[ddraw]\n; ### Optional settings ###\n; Use the following settings to adjust the look and feel to your liking\n\n\n; Stretch to custom resolution, 0 = defaults to the size game requests\nwidth=0\nheight=0\n\n; Override the width/height settings shown above and always stretch to fullscreen\n; Note: Can be combined with 'windowed=true' to get windowed-fullscreen aka borderless mode\nfullscreen=false\n\n; Run in windowed mode rather than going fullscreen\nwindowed=false\n\n; Maintain aspect ratio - (Requires 'handlemouse=true')\nmaintas=false\n\n; Windowboxing / Integer Scaling - (Requires 'handlemouse=true')\nboxing=false\n\n; Real rendering rate, -1 = screen rate, 0 = unlimited, n = cap\n; Note: Does not have an impact on the game speed, to limit your game speed use 'maxgameticks='\nmaxfps=125\n\n; Vertical synchronization, enable if you get tearing - (Requires 'renderer=auto/opengl/direct3d9')\n; Note: vsync=true can fix tearing but it will cause input lag\nvsync=false\n\n; Automatic mouse sensitivity scaling  - (Requires 'handlemouse=true')\n; Note: Only works if stretching is enabled. Sensitivity will be adjusted according to the size of the window\nadjmouse=false\n\n; Preliminary libretro shader support - (Requires 'renderer=opengl') https://github.com/libretro/glsl-shaders\n; 2x scaling example: https://imgur.com/a/kxsM1oY - 4x scaling example: https://imgur.com/a/wjrhpFV\nshader=\t;Shaders\\interpolation\\bilinear.glsl\n\n; Window position, -32000 = center to screen\nposX=-32000\nposY=-32000\n\n; Renderer, possible values: auto, opengl, gdi, direct3d9 (auto = try direct3d9/opengl, fallback = gdi)\nrenderer=auto\n\n; Developer mode (don't lock the cursor)\ndevmode=false\n\n; Show window borders in windowed mode\nborder=true\n\n; Save window position/size/state on game exit and restore it automatically on next game start\n; Possible values: 0 = disabled, 1 = save to global 'ddraw' section, 2 = save to game specific section\nsavesettings=1\n\n; Should the window be resizeable by the user in windowed mode?\nresizeable=true\n\n; Enable C&C video resize hack - Stretches C&C cutscenes to fullscreen\nvhack=false\n\n; Enable linear (D3DTEXF_LINEAR) upscaling filter for the direct3d9 renderer (16 bit color depth games only)\nd3d9linear=true\n\n\n\n; ### Compatibility settings ###\n; Use the following settings in case there are any issues with the game\n\n\n; Hide WM_ACTIVATEAPP and WM_NCACTIVATE messages to prevent problems on alt+tab\nnoactivateapp=false\n\n; Max game ticks per second, possible values: -1 = disabled, 0 = emulate 60hz vblank, 1-1000 = custom game speed\n; Note: Can be used to slow down a too fast running game, fix flickering or too fast animations\n; Note: Usually one of the following values will work: 60 / 30 / 25 / 20 / 15 (lower value = slower game speed)\nmaxgameticks=0\n\n; Gives cnc-ddraw full control over the mouse cursor (required for adjmouse/boxing/maintas)\n; Note: Set this to 'false' if your cursor becomes invisible at some places in the game\nhandlemouse=true\n\n; Windows API Hooking, Possible values: 0 = disabled, 1 = IAT Hooking, 2 = Microsoft Detours, 3 = IAT+Detours Hooking (All Modules), 4 = IAT Hooking (All Modules)\n; Note: Change this value if windowed mode or upscaling isn't working properly\n; Note: 'hook=2' will usually work for problematic games, but 'hook=2' must be combined with renderer=gdi\nhook=4\n\n; Force minimum FPS, possible values: 0 = disabled, -1 = use 'maxfps=' value, 1-1000 = custom FPS\n; 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)\nminfps=0\n\n; Disable fullscreen-exclusive mode for the direct3d9/opengl renderers\n; Note: Can be used in case some GUI elements like buttons/textboxes/videos/etc.. are invisible\nnonexclusive=false\n\n; Force CPU0 affinity, avoids crashes/freezing, *might* have a performance impact\nsinglecpu=true\n\n\n\n; ### Game specific settings ###\n; The following settings override all settings shown above, section name = executable name\n\n\n; Command & Conquer: Red Alert - CnCNet\n[ra95-spawn]\nmaxfps=125\n\n; Command & Conquer Gold - CnCNet\n[cnc95]\nmaxfps=125\n\n; Carmageddon\n[CARMA95]\nrenderer=opengl\nnoactivateapp=true\nmaxgameticks=60\n\n; Carmageddon\n[CARM95]\nrenderer=opengl\nnoactivateapp=true\nmaxgameticks=60\n\n; Command & Conquer Gold\n[C&C95]\nmaxgameticks=120\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert\n[ra95]\nmaxgameticks=120\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert\n[ra95p]\nmaxfps=60\nminfps=-1\n\n; Age of Empires\n[empires]\nhandlemouse=false\n\n; Age of Empires: The Rise of Rome\n[empiresx]\nhandlemouse=false\n\n; Age of Empires II\n[EMPIRES2]\nhandlemouse=false\n\n; Age of Empires II: The Conquerors\n[age2_x1]\nhandlemouse=false\n\n; Outlaws\n[olwin]\nnoactivateapp=true\nmaxgameticks=60\nhandlemouse=false\nrenderer=gdi\n\n; Dark Reign: The Future of War\n[DKReign]\nmaxgameticks=60\n\n; Star Wars: Galactic Battlegrounds\n[battlegrounds]\nhandlemouse=false\n\n; Star Wars: Galactic Battlegrounds: Clone Campaigns\n[battlegrounds_x1]\nhandlemouse=false\n\n; Carmageddon 2\n[Carma2_SW]\nrenderer=opengl\nnoactivateapp=true\nmaxgameticks=60\n\n; Atomic Bomberman\n[BM]\nmaxgameticks=60\n\n; Dune 2000\n[dune2000]\nmaxfps=59\naccuratetimers=true\n\n; Dune 2000 - CnCNet\n[dune2000-spawn]\nmaxfps=59\naccuratetimers=true\n\n; Command & Conquer: Tiberian Sun / Command & Conquer: Red Alert 2\n[game]\ncheckfile=.\\blowfish.dll\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Tiberian Sun Demo\n[SUN]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Tiberian Sun - CnCNet\n[ts-spawn]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2 - XWIS\n[ra2]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2 - XWIS\n[Red Alert 2]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2: Yuri's Revenge\n[gamemd]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2: Yuri's Revenge - ?ModExe?\n[ra2md]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2: Yuri's Revenge - CnCNet\n[gamemd-spawn]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Command & Conquer: Red Alert 2: Yuri's Revenge - XWIS\n[Yuri's Revenge]\nnoactivateapp=true\nhandlemouse=false\nmaxfps=60\nminfps=-1\n\n; Twisted Metal\n[TWISTED]\nrenderer=opengl\nnonexclusive=true\nmaxgameticks=25\nminfps=5\n\n; Twisted Metal 2\n[Tm2]\nrenderer=opengl\nnonexclusive=true\nmaxgameticks=60\nhandlemouse=false\nfixchildwindows=false\n\n; Caesar III\n[c3]\nhandlemouse=false\nsierrahack=true\n\n; Pharaoh\n[Pharaoh]\nhandlemouse=false\nsierrahack=true\n\n; Master of Olympus - Zeus\n[Zeus]\nhandlemouse=false\nsierrahack=true\nrenderer=gdi\nhook=2\n\n; Dungeon Keeper 2\n[DKII]\nmaxgameticks=60\nnoactivateapp=true\ndk2hack=true\n\n; Chris Sawyer's Locomotion\n[LOCO]\nhandlemouse=false\n\n; Age of Wonders\n[AoWSM]\nwindowed=true\nfullscreen=false\nrenderer=gdi\nhook=2\n\n; Age of Wonders 2\n[AoW2]\nwindowed=true\nfullscreen=false\nrenderer=gdi\nhook=2\n\n; Stronghold Crusader HD\n[Stronghold Crusader]\nhandlemouse=false\n\n; Stronghold Crusader Extreme HD\n[Stronghold_Crusader_Extreme]\nhandlemouse=false\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-auto.ini",
    "content": "; ts-ddraw - https://github.com/CnCNet/ts-ddraw\n\n; use the following settings to enable the experimental stretching support\n; works only fullscreen right now + menus are not centered\n\n[ddraw]\n; stretch to custom resolution, 0 = defaults to the size game requests\n;StretchToWidth=2560\n;StretchToHeight=1440\n\n; override StretchToWidth/StretchToHeight and always stretch to fullscreen\n;StretchToFullscreen=No\n\n; use windowboxing to make a best fit\n;Windowboxing=No\n\n; maintain aspect ratio - this setting is ignored when Windowboxing is enabled\n;MaintainAspectRatio=No\n\n; Enable vertical sync\n;VSync=No\n\n; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI\nRenderer=auto\n\n; Draw the FPS overlay 0 = no, 1 = yes\n;DrawFPS=0\n\n; Use 2 textures, in rotation, for the Primary Surface, default = yes\n;PrimarySurface2Tex=Yes\n\n; Use single processor affinity to prevent thread crashes\n;SingleProcAffinity=Yes\n\n; Set the Renderer FPS value, Default = 60, unless VSync\n;TargetFPS=60\n\n; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch.\n; FixedOutput=stretch\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-gdi.ini",
    "content": "; ts-ddraw - https://github.com/CnCNet/ts-ddraw\n\n; use the following settings to enable the experimental stretching support\n; works only fullscreen right now + menus are not centered\n\n[ddraw]\n; stretch to custom resolution, 0 = defaults to the size game requests\n;StretchToWidth=2560\n;StretchToHeight=1440\n\n; override StretchToWidth/StretchToHeight and always stretch to fullscreen\n;StretchToFullscreen=No\n\n; use windowboxing to make a best fit\n;Windowboxing=No\n\n; maintain aspect ratio - this setting is ignored when Windowboxing is enabled\n;MaintainAspectRatio=No\n\n; Enable vertical sync\n;VSync=No\n\n; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI\nRenderer=gdi\n\n; Draw the FPS overlay 0 = no, 1 = yes\n;DrawFPS=0\n\n; Use 2 textures, in rotation, for the Primary Surface, default = yes\n;PrimarySurface2Tex=Yes\n\n; Use single processor affinity to prevent thread crashes\n;SingleProcAffinity=Yes\n\n; Set the Renderer FPS value, Default = 60, unless VSync\n;TargetFPS=60\n\n; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch.\n; FixedOutput=stretch\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-opengl.ini",
    "content": "; ts-ddraw - https://github.com/CnCNet/ts-ddraw\n\n; use the following settings to enable the experimental stretching support\n; works only fullscreen right now + menus are not centered\n\n[ddraw]\n; stretch to custom resolution, 0 = defaults to the size game requests\n;StretchToWidth=2560\n;StretchToHeight=1440\n\n; override StretchToWidth/StretchToHeight and always stretch to fullscreen\n;StretchToFullscreen=No\n\n; use windowboxing to make a best fit\n;Windowboxing=No\n\n; maintain aspect ratio - this setting is ignored when Windowboxing is enabled\n;MaintainAspectRatio=No\n\n; Enable vertical sync\n;VSync=No\n\n; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI\nRenderer=opengl\n\n; Draw the FPS overlay 0 = no, 1 = yes\n;DrawFPS=0\n\n; Use 2 textures, in rotation, for the Primary Surface, default = yes\n;PrimarySurface2Tex=Yes\n\n; Use single processor affinity to prevent thread crashes\n;SingleProcAffinity=Yes\n\n; Set the Renderer FPS value, Default = 60, unless VSync\n;TargetFPS=60\n\n; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch.\n; FixedOutput=stretch\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Configs/dxwnd.ini",
    "content": "[DxWnd]\nEnabled=Yes\nEmulation=1\nDisableMaxWindowedMode=Yes\nRunInWindow=True\nClipCursor=Yes\nForceClipper=Yes\nNoWindowFrame=False\nForceDirectDrawEmulation=No\n\n[keymapping]\nrefresh=0x74\nworkarea=0x7B\nfullscreen=0x0D"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Unix/wine-game.bat",
    "content": "Syringe.exe \"gamemd.exe\" -SPAWN -CD -LOG"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Unix/wine-game.sh",
    "content": "#!/bin/sh\nwineconsole Resources/Compatibility/Unix/wine-game.bat &\nBACK_PID=$!\nwait $BACK_PID\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Unix/wine-mapedit.bat",
    "content": "cd \"Map Editor\"\nSyringe.exe \"REAlert2.dat\"\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Compatibility/Unix/wine-mapedit.sh",
    "content": "#!/bin/sh\nwineconsole Resources/Compatibility/Unix/wine-mapedit.bat &\nBACK_PID=$!\nwait $BACK_PID\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Default Theme/DTACnCNetClient.ini",
    "content": "; Dawn of the Tiberium Age (DTA) CnCNet Client settings\n; Created by Rampastring\n; http://www.moddb.com/members/rampastring\n; If you use or redistribute the client in any public projects, please include\n; Rampastring and Dawn of the Tiberium Age in your project's credits.\n\n; Use DTACnCNetClient.ini to configure theme-specific settings.\n\n[General]\n; The color of most strings in the UI.\nUILabelColor=181,251,0\n; The foreground color of usable (buttons, drop-down boxes, chat boxes etc.) UI items.\nAltUIColor=111,251,0\n; The background color of usable UI items.\nAltUIBackgroundColor=0,15,8\n; The color into which the font of buttons will change into when you hover over them (in RGB).\nButtonHoverColor=252,252,252\n; The color of admins' names on the channel player lists.\nAdminNameColor=255,0,0\n; The default color of chat messages. Used for messages which lack color information (rare), as well as for private messages.\nDefaultChatColor=0,255,0\nDefaultPersonalChatColorIndex=1\n; The color of sent private messages. \nPrivateMessageColor=160,160,160\n; The color of received private messages.\nPrivateMessageOtherUserColor=64,196,64\n;defaults to 64,64,168\nListBoxFocusColor=40,68,0\nHoverOnGameColor=20,34,18\nPanelBorderColor=74,182,222\nWindowBorderColor=111,251,0\n; Button fade animations (1.0 = disabled)\nAlphaRate=1.0\n; Check-box tick animations\nCheckBoxAlphaRate=0.2\n;R,G,B,A\nMapPreviewNameBackgroundColor=0,0,0,144\nMapPreviewNameBorderColor=181,251,0,144\nMainMenuTheme=Default Theme\\MainMenuTheme\nStartingLocationsUsePlayerRemapColor=yes\n\n[GameLobby]\n; Color of the Co-Op briefing text and outline in R,G,B\nCoopBriefingForeColor=0,255,0\n; Color to use when a non-default option is selected in a combo box. Defaults to red.\nComboBoxNondefaultColor=255,255,255\n\n[ParserConstants]\nEMPTY_SPACE_TOP=12\nEMPTY_SPACE_BOTTOM=11\nEMPTY_SPACE_SIDES=22\nLOBBY_PANEL_SPACING=10\nBUTTON_SPACING=12\nCHECKBOX_SPACING=25\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/ExtrasWindow.ini",
    "content": "[ExtrasWindow]\nSize=1280,720\nDrawBorders=false\nBackgroundTexture=MainMenu/mainmenuebg.png\n\n[ExtraControls]\n0=Logo:XNAExtraPanel\n1=btnLan:XNALinkButton\n\n[Logo]\nLocation=369,8\nBackgroundTexture=MainMenu/Logo.png\n\n[btnLan]\nLocation=490,261\nIdleTexture=MainMenu/lan.png\nHoverTexture=MainMenu/lan_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnExMapEditor]\nLocation=490,315\nIdleTexture=MainMenu/mapeditor.png\nHoverTexture=MainMenu/mapeditor_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnExStatistics]\nLocation=490,369\nIdleTexture=MainMenu/statistics.png\nHoverTexture=MainMenu/statistics_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnExCredits]\nLocation=490,423\nIdleTexture=MainMenu/credits.png\nHoverTexture=MainMenu/credits_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnExCancel]\nLocation=490,477\nIdleTexture=MainMenu/back.png\nHoverTexture=MainMenu/back_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[lblCnCNetStatus]\nText=TS Players on CnCNet\nRemapColor=45,228,255\nLocation=1099,67\nDistanceFromRightBorder=149\n\n[lblCnCNetPlayerCount]\nRemapColor=45,228,255\nLocation=990,67\nDistanceFromRightBorder=286\n\n[txtVersion]\nText=Version:\nRemapColor=45,228,255\nLocation=990,91\nDistanceFromRightBorder=252\n\n[lblVersion]\nRemapColor=45,228,255\nIdleColor=45,228,255\nHoverColor=255,255,255\nLocation=1304,91\nDistanceFromRightBorder=246\n\n[lblUpdateStatus]\nRemapColor=45,228,255\nIdleColor=45,228,255\nHoverColor=255,255,255\nLocation=990,115\nDistanceFromRightBorder=129\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/GameCollectionConfig.ini",
    "content": "[CustomGames]\n0=PP\n1=UNKN\n\n[PP]\nInternalName=pp\nUIName=Project Phantom\nChatChannel=#projectphantom\nGameBroadcastChannel=#projectphantom-games\nClientExecutableName=PPLauncher.exe\nRegistryInstallPath=HKLM\\Software\\ProjectPhantom\nIconFilename=ppicon.png\n\n[UNKN]\nInternalName=unkn\nUIName=Unknown (Example)\nChatChannel=#unkn\nGameBroadcastChannel=#unkn-games\nClientExecutableName=unknown.exe\nRegistryInstallPath=HKLM\\Software\\nonexistent-99aed32b-4428-4724-8f49-e8aa0e87c675\nIconFilename=nonexistent-99aed32b-4428-4724-8f49-e8aa0e87c675.png\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/GameLobbyBase.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[SkirmishLobby]\nBackgroundTexture=MainMenu/dbak.png\nDrawMode=Centered\n;SolidColorBackgroundTexture=0,24,72,128\nPlayerOptionLocationX=11\t;22    ;def=25\nPlayerOptionLocationY=25    ;def=24\nPlayerOptionVerticalMargin=9    ;def=12\nPlayerOptionHorizontalMargin=5    ;def=3\nPlayerOptionCaptionLocationY=6    ;def=6\nPlayerNameWidth=128\t;117; def=136\nSideWidth=86    ;def=91\nColorWidth=70    ;def=79\nStartWidth=0    ;def=49\nTeamWidth=44    ;def=46\n\n$CC00=btnLaunchGame:GameLaunchButton\n$CC01=btnLeaveGame:XNAClientButton\n$CC03=MapPreviewBox:MapPreviewBox\n$CC04=GameOptionsPanel:XNAPanel\n$CC05=PlayerOptionsPanel:XNAPanel\n$CC06=lblMapName:XNALabel\n$CC07=lblMapAuthor:XNALabel\n$CC08=lblGameMode:XNALabel\n$CC09=lblMapSize:XNALabel\n$CC12=lbMapList:XNAMultiColumnListBox\n$CC13=lblGameModeSelect:XNALabel\n$CC14=ddGameMode:XNAClientDropDown\n$CC15=tbMapSearch:XNASuggestionTextBox\n$CC16=btnPickRandomMap:XNAClientButton\n$CC17=btnSaveLoadGameOptions:XNAClientButton\n\n[btnLaunchGame]\nText=Launch Game\n;TextShadowDistance=2\n$Width=133\n$X=EMPTY_SPACE_SIDES\n$Y=getHeight($ParentControl) - getHeight($Self) - EMPTY_SPACE_BOTTOM\n\n[btnLeaveGame]\n$Width=133\n;TextShadowDistance=2\n$X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES\n$Y=getY(btnLaunchGame)\nText=Main Menu\n\n[MapPreviewBox]\nSolidColorBackgroundTexture=0,0,0,192\n$Width=802\n$X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES\n$Y=316\n$Height=getHeight($ParentControl) - getY($Self) - 46\n\n[lblMapName]\nText=Map:\nFontIndex=1\n$TextAnchor=CENTER\n$AnchorPoint=getX(MapPreviewBox) + (getWidth(MapPreviewBox) / 2),getY(MapPreviewBox) - getHeight($Self)\n\n[lblMapAuthor]\nFontIndex=1\n$TextAnchor=LEFT\n$AnchorPoint=getRight(MapPreviewBox),getY(lblMapName)\n\n[lblGameMode]\nFontIndex=1\n$TextAnchor=RIGHT\n$AnchorPoint=getX(MapPreviewBox),getY(lblMapName)\n\n[lblMapSize]\nFontIndex=0\t;3\n$TextAnchor=LEFT\n$AnchorPoint=getRight(MapPreviewBox) - 3,getBottom(MapPreviewBox) - 16\n\n[lbMapList]\nSolidColorBackgroundTexture=0,0,0,192\n$X=EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP + 28\n$Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING)\n$Height=getBottom(MapPreviewBox) - getY($Self)\n\n[lblGameModeSelect]\nText=GAME MODE:\nFontIndex=1\n$TextAnchor=RIGHT\n$AnchorPoint=getX(lbMapList),getY(lbMapList) - getHeight($Self) - 8\n\n[ddGameMode]\n$Width=150\n$Height=21\n$X=getRight(lbMapList) - getWidth($Self)\n$Y=getY(lbMapList) - getHeight($Self) - 7\n\n[tbMapSearch]\nSuggestion=Search map...\n$Width=getWidth(lbMapList) - 15\n$Height=19\n$X=getX(lbMapList) + 15\n$Y=getY(lbMapList)\n\n[btnPickRandomMap]\nText=Random\n;TextShadowDistance=2\n$Width=75\n$Height=17\n$X=getRight(lbMapList) - getWidth($Self) - 1\n$Y=getY(lbMapList) + 1\n\n[PlayerOptionsPanel]\nSolidColorBackgroundTexture=0,0,0,192\nDrawBorders=yes\n$X=getX(MapPreviewBox)\n$Y=EMPTY_SPACE_TOP\n$Width=getWidth($ParentControl) - (getX($Self) + (getWidth(GameOptionsPanel) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING)\t;365\n$Height=getHeight(GameOptionsPanel)\n\n[btnSaveLoadGameOptions]\nIdleTexture=comboBoxArrow.png\nHoverTexture=comboBoxArrow.png\n$Width=18\n$Height=21\n$X=getRight(GameOptionsPanel) - getWidth($Self) - 1\n$Y=getY(GameOptionsPanel) + 1\n\n[GameOptionsPanel]\nSolidColorBackgroundTexture=0,0,0,192\nDrawBorders=yes\n$Width=427\n$Height=266\n$X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP\n$CC_00=cmbTSFS:GameLobbyDropDown\n$CC_01=lblTSFS:XNALabel\n$CC_02=cmbTechLevel:GameLobbyDropDown\n$CC_03=lblTechLevel:XNALabel\n$CC_04=cmbCredits:GameLobbyDropDown\n$CC_05=lblCredits:XNALabel\n$CC_06=cmbUnitCount:GameLobbyDropDown\n$CC_07=lblUnitCount:XNALabel\n$CC_08=cmbGameSpeedCap:GameLobbyDropDown\n$CC_09=lblGameSpeedCap:XNALabel\n$CC_10=chkBases:GameLobbyCheckBox\n$CC_11=chkShortGame:GameLobbyCheckBox\n$CC_12=chkRedeplMCV:GameLobbyCheckBox\n$CC_13=chkMultiEng:GameLobbyCheckBox\n$CC_14=chkDestrBridges:GameLobbyCheckBox\n$CC_15=chkCrates:GameLobbyCheckBox\n$CC_16=chkVisceroids:GameLobbyCheckBox\n$CC_17=chkNoBaddyCrates:GameLobbyCheckBox\n$CC_18=chkAttackNeutralUnits:GameLobbyCheckBox\n$CC_19=chkIngameAllying:XNACheckBox\n$CC_20=chkBuildOffAlly:GameLobbyCheckBox\n$CC_21=chkHarderAI:GameLobbyCheckBox\n$CC_22=chkVetBal:GameLobbyCheckBox\n$CC_23=chkInfiniteTiberium:GameLobbyCheckBox\n$CC_24=chkImmuneHarvs:GameLobbyCheckBox\n$CC_25=chkSilos:GameLobbyCheckBox\n$CC_26=chkAimableSams:GameLobbyCheckBox\n$CC_27=chkMultipleFactory:GameLobbyCheckBox\n$CC_28=chkSuperWeapons:GameLobbyCheckBox\n$CC_29=chkRevealShroud:GameLobbyCheckBox\n\n[cmbTSFS]\n$Width=97\n$Height=21\n$X=16\n$Y=26\nItems=Tiberian Sun,Firestorm\nDefaultIndex=0\nSpawnIniOption=Firestorm\nDataWriteMode=Boolean\n\n[lblTSFS]\nText=Game Type:\n$X=getX(cmbTSFS)\n$Y=getY(cmbTSFS) - 19\n\n[cmbTechLevel]\n$Width=97\n$Height=21\n$X=getX(cmbTSFS)\n$Y=getY(cmbTSFS) + 52\nOptionName=Tech Level\nItems=10,9,8,7,6,5,4,3,2,1\nDefaultIndex=0\nSpawnIniOption=TechLevel\nDataWriteMode=String\n\n[lblTechLevel]\nText=Tech Level:\n$X=getX(cmbTechLevel)\n$Y=getY(cmbTechLevel) - 19\n\n[cmbCredits]\n$Width=97\n$Height=21\n$X=getX(cmbTSFS)\n$Y=getY(cmbTechLevel) + 52\nOptionName=Starting Credits\nItems=20000,15000,12500,10000,7500,5000,2500\nDefaultIndex=3\nSpawnIniOption=Credits\nDataWriteMode=String\n\n[lblCredits]\nText=Starting Credits:\n$X=getX(cmbCredits)\n$Y=getY(cmbCredits) - 19\n\n[cmbUnitCount]\n$Width=97\n$Height=21\n$X=getX(cmbTSFS)\n$Y=getY(cmbCredits) + 52\nOptionName=Unit Count\nItems=10,9,8,7,6,5,4,3,2,1\nDefaultIndex=9\nSpawnIniOption=UnitCount\nDataWriteMode=String\n\n[lblUnitCount]\nText=Unit Count:\n$X=getX(cmbUnitCount)\n$Y=getY(cmbUnitCount) - 19\n\n[cmbGameSpeedCap]\n$Width=97\n$Height=21\n$X=getX(cmbTSFS)\n$Y=getY(cmbUnitCount) + 52\nOptionName=Game Speed\nItems=Maximum,60 FPS,45 FPS,30 FPS,20 FPS,15 FPS,10 FPS\nDefaultIndex=1\nSpawnIniOption=GameSpeed\nDataWriteMode=Index\n\n[lblGameSpeedCap]\nText=Game Speed:\n$X=getX(cmbGameSpeedCap)\n$Y=getY(cmbGameSpeedCap) - 19\n\n[chkBases]\nText=Bases\nSpawnIniOption=Bases\nChecked=True\nToolTip=Players start with Mobile Construction Vehicles.\n$X=133\n$Y=11\n\n[chkShortGame]\nText=Short Game\nSpawnIniOption=ShortGame\nChecked=True\nToolTip=Having only units and no structures left will cause the units to self-destruct and make the player instantly lose the game.\n$X=getX(chkBases)\n$Y=getY(chkBases) + CHECKBOX_SPACING\n\n[chkRedeplMCV]\nText=Re-Deployable MCV\nSpawnIniOption=MCVRedeploy\nChecked=True\nToolTip=Construction Yards can repack into a Mobile Construction Vehicle.\n$X=getX(chkShortGame)\n$Y=getY(chkShortGame) + CHECKBOX_SPACING\n\n[chkMultiEng]\nText=Multi Engineer\nSpawnIniOption=MultiEngineer\nChecked=False\nToolTip=Capturing a structure requires three Engineers instead of one.\n$X=getX(chkShortGame)\n$Y=getY(chkRedeplMCV) + CHECKBOX_SPACING\n\n[chkDestrBridges]\nText=Destroyable Bridges\nSpawnIniOption=BridgeDestroy\nChecked=True\nToolTip=You can destroy low bridges by force-firing on them.\n$X=getX(chkShortGame)\n$Y=getY(chkMultiEng) + CHECKBOX_SPACING\n\n[chkCrates]\nText=Crates\nSpawnIniOption=Crates\nChecked=True\nToolTip=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.\n$X=getX(chkShortGame)\n$Y=getY(chkDestrBridges) + CHECKBOX_SPACING\n\n[chkNoBaddyCrates]\nText=Safe Crates Only\nCustomIniPath=INI/Game Options/No Baddy Crates.ini\nVisible=False\nChecked=False\nToolTip=No crates with potential negative effects will appear if crates are enabled.\n$X=getX(chkShortGame)\n$Y=getY(chkCrates) + CHECKBOX_SPACING\n\n[chkVisceroids]\nText=Visceroids\nReversed=yes\t;make the checkbox set the associated key to =False instead of =True when enabled\nCustomIniPath=INI/Game Options/Disable Visceroids.ini\nChecked=True\nToolTip=Infantry that die from walking over tiberium will become visceroids and some maps will already have visceroids present from the start.\n$X=getX(chkShortGame)\n$Y=getY(chkCrates) + CHECKBOX_SPACING\n\n[chkAttackNeutralUnits]\nText=Auto-target Neutrals\nSpawnIniOption=AttackNeutralUnits\nChecked=True\nToolTip=Units automatically attack armed neutral units.\n$X=getX(chkShortGame)\n$Y=getY(chkVisceroids) + CHECKBOX_SPACING\n\n[chkIngameAllying]\nText=Ingame Allying\t;Locked Teams\nSpawnIniOption=AlliesAllowed\nChecked=False\nEnabled=False\nAllowChecking=false\n;Reversed=true\nToolTip=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.\n$X=getX(chkShortGame)\n$Y=getY(chkAttackNeutralUnits) + CHECKBOX_SPACING\n\n[chkBuildOffAlly]\nText=Build Off Ally\nSpawnIniOption=BuildOffAlly\nChecked=False\nToolTip=Allow building next to structures of teammates.\n$X=getX(chkShortGame)\n$Y=getY(chkIngameAllying) + CHECKBOX_SPACING\n\n[chkHarderAI]\nText=Harder AI\nCustomIniPath=INI/Game Options/Harder AI.ini\nChecked=False\nToolTip=The AI is much harder than the default AI of Tiberian Sun.\n$X=281\n$Y=getY(chkBases)\n\n[chkVetBal]\nText=Veteran Balance Patch\nCustomIniPath=INI/Game Options/Veteran Balance Patch.ini\nChecked=False\nToolTip=The game will be rebalanced according to Veteran Balance Patch v2.50.\n$X=getX(chkHarderAI)\n$Y=getY(chkHarderAI) + CHECKBOX_SPACING\n\n[chkInfiniteTiberium]\nText=Infinite Tiberium\nCustomIniPath=INI/Game Options/Infinite Tiberium.ini\nChecked=False\nToolTip=Tiberium is much more valuable and lasts longer than normally.\nMapScoringMode=DenyWhenChecked\n$X=getX(chkHarderAI)\n$Y=getY(chkVetBal) + CHECKBOX_SPACING\n\n[chkImmuneHarvs]\nText=Immune Harvesters\nCustomIniPath=INI/Game Options/Immune Harvesters.ini\nChecked=False\nToolTip=Harvesters are indestructible (but are no longer able to crush infantry).\n;Visible=False\n;Enabled=False\nMapScoringMode=DenyWhenChecked\n$X=getX(chkHarderAI)\n$Y=getY(chkInfiniteTiberium) + CHECKBOX_SPACING\n\n[chkSilos]\nText=Silos Needed\nCustomIniPath=INI/Game Options/No Silos.ini\nChecked=True\nReversed=True\nToolTip=You don't need to build extra Refineries/Silos to store a lot of credits.\n$X=getX(chkHarderAI)\n$Y=getY(chkImmuneHarvs) + CHECKBOX_SPACING\n\n[chkAimableSams]\nText=Aimable SAMs\nSpawnIniOption=AimableSams\nChecked=False\nToolTip=Allow players to give orders to target specific aircraft with the selected SAM Site(s).\n$X=getX(chkHarderAI)\n$Y=getY(chkSilos) + CHECKBOX_SPACING\n\n[chkMultipleFactory]\nText=Multiple Factory Bonus\nSpawnIniOption=MultipleFactory\nEnabledSpawnIniValue=.85\nDisabledSpawnIniValue=0\nChecked=False\nToolTip=Building multiple factories will speed up production of whatever that factory can produce by 17.6% per factory.\n$X=getX(chkHarderAI)\n$Y=getY(chkAimableSams) + CHECKBOX_SPACING\n\n[chkSuperWeapons]\nText=Super Weapons\nReversed=yes\t;make the checkbox set the associated key to =False instead of =True when enabled\nCustomIniPath=INI/Game Options/Disable Super Weapons.ini\nChecked=True\nToolTip=Players can use super weapons such as the multi-missile and ion cannon.\n$X=getX(chkHarderAI)\n$Y=getY(chkMultipleFactory) + CHECKBOX_SPACING\n\n[chkRevealShroud]\nText=Revealed Map\nCustomIniPath=INI/Game Options/Reveal Shroud.ini\nChecked=False\nToolTip=The map will be entirely unshrouded when the game starts.\nMapScoringMode=DenyWhenChecked\n$X=getX(chkHarderAI)\n$Y=getY(chkSuperWeapons) + CHECKBOX_SPACING"
  },
  {
    "path": "DXMainClient/Resources/DTA/GameOptions.ini",
    "content": "; The Dawn of the Tiberium Age (DTA) CnCNet Client settings\n; Created by Rampastring\n; http://www.moddb.com/members/rampastring\n; If you use or redistribute the client in any public projects, please include\n; Rampastring and Dawn of the Tiberium Age in your project's credits.\n\n[General]\nSides=GDI,Nod\nStartingLocationAngularVelocity=0.0075\nReservedStartingLocationAngularVelocity=0.05\nRandomColor=168,168,168\n\n; The multiplayer colors. Syntax: <Name>=R,G,B,<in-game color ID>\n[MPColors]\nGold=255,223,94,0\nRed=222,0,0,1\nBlue=39,60,179,2\nGreen=12,150,12,3\nOrange=255,145,0,4\nCyan=20,177,255,5\nPurple=185,20,255,6\nPink=255,94,199,7\n\n; Keys that are ALWAYS written to spawn.ini when the game starts.\n; These can be overriden by gamemode-specific code.\n[ForcedSpawnIniOptions]\n;Bases=Yes\nFogOfWar=No\nSidebarHack=Yes\nProtocol=0"
  },
  {
    "path": "DXMainClient/Resources/DTA/GenericWindow.ini",
    "content": "[GenericWindow]\nBackgroundTexture=MainMenu/dbak.png\nDrawMode=Centered\t;Stretched\nDrawBorders=false\n\n[LoadingScreen]\nSize=1280,720\n\n[GameCreationWindow]\nBackgroundTexture=MainMenu/dbak.png\nSize=490,205\nDrawMode=Centered\t;Stretched\nDrawBorders=false\n\n[ExtraControls]\n00=bar_ul:XNAExtraPanel\n01=bar_ur:XNAExtraPanel\n02=bar_lr:XNAExtraPanel\n03=bar_ll:XNAExtraPanel\n04=rightbar:XNAExtraPanel\n05=leftbar:XNAExtraPanel\n06=glow_t:XNAExtraPanel\n07=glow_b:XNAExtraPanel\n08=glow_l:XNAExtraPanel\n09=glow_r:XNAExtraPanel\n10=glow_tl:XNAExtraPanel\n11=glow_tr:XNAExtraPanel\n12=glow_bl:XNAExtraPanel\n13=glow_br:XNAExtraPanel\n\n[bar_ul]\nLocation=-24,0\nBackgroundTexture=bar_ul.png\n\n[bar_ur]\nBackgroundTexture=bar_ur.png\nLocation=0,0\nDistanceFromRightBorder=-24 ; overrides the Location= key's X-coordinate\n\n[bar_lr]\nBackgroundTexture=bar_lr.png\nDistanceFromRightBorder=-24\nDistanceFromBottomBorder=0\n\n[bar_ll]\nLocation=-24,0\nBackgroundTexture=bar_ll.png\nDistanceFromBottomBorder=0\n\n[rightbar]\nLocation=0,12\nBackgroundTexture=rightbar.png\nDistanceFromRightBorder=-24\nFillHeight=12\n\n[leftbar]\nLocation=-24,12\nBackgroundTexture=leftbar.png\nFillHeight=12\n\n[glow_t]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=16,0\nBackgroundTexture=glow_t.png\nFillWidth=16\n\n[glow_b]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=16,0\nBackgroundTexture=glow_b.png\nDistanceFromBottomBorder=0\nFillWidth=16\n\n[glow_l]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,16\nBackgroundTexture=glow_l.png\nFillHeight=16\n\n[glow_r]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,16\nBackgroundTexture=glow_r.png\nDistanceFromRightBorder=0\nFillHeight=16\n\n[glow_tl]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,0\nBackgroundTexture=glow_tl.png\n\n[glow_tr]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,0\nBackgroundTexture=glow_tr.png\nDistanceFromRightBorder=0\n\n[glow_bl]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,0\nBackgroundTexture=glow_bl.png\nDistanceFromBottomBorder=0\n\n[glow_br]\nRemapColor=192,192,192,192\t;extra transparency\nLocation=0,0\nBackgroundTexture=glow_br.png\nDistanceFromBottomBorder=0\nDistanceFromRightBorder=0\n\n\n[PrivacyNotification]\nBackgroundTexture=MainMenu/dbak.png\nDrawMode=Centered\nFillWidth=48\nLocation=24,0\nDistanceFromBottomBorder=0\n;Width=920\t;FillWidth=48\n;Location=180,317\t;24,317\nDrawBorders=false\n\n[btnOK]\nDistanceFromRightBorder=25\n\n[SkirmishLobby]\n;DrawMode=Tiled\n$Width=RESOLUTION_WIDTH - 40  ; Defines the width of the window\n$Height=RESOLUTION_HEIGHT - 34 ; Defines the height of the window\nDrawBorders=false\n\n;[MultiplayerGameLobby] is controlled by $BaseSection=SkirmishLobby in LANGameLobby.ini\n\n[$ExtraControls]\n;$CCbg=Background:XNAPanel\n$CCbar_ul=winbar_ul:XNAPanel\n$CCbar_ur=winbar_ur:XNAPanel\n$CCbar_lr=winbar_lr:XNAPanel\n$CCbar_ll=winbar_ll:XNAPanel\n$CCbar_l=winbar_r:XNAPanel\n$CCbar_r=winbar_l:XNAPanel\n$CCglow_tl=glow_top_left:XNAPanel\n$CCglow_tr=glow_top_right:XNAPanel\n$CCglow_bl=glow_bottom_left:XNAPanel\n$CCglow_br=glow_bottom_right:XNAPanel\n$CCglow_t=glow_top:XNAPanel\n$CCglow_b=glow_bottom:XNAPanel\n$CCglow_l=glow_left:XNAPanel\n$CCglow_r=glow_right:XNAPanel\n\n[Background]\nDrawMode=Tiled\nDrawBorders=false\n$Width=getWidth($ParentControl)\n$Height=getHeight($ParentControl) + 4\n$X=0\n$Y=-2\nDrawOrder=-2000\nUpdateOrder=-2000\n\n[winbar_ul]\nBackgroundTexture=bar_ul.png\n$Width=24\n$Height=12\nDrawBorders=false\n$X=- (getWidth($Self) - 4)\n$Y=0\n\n[winbar_ur]\nBackgroundTexture=bar_ur.png\n$Width=24\n$Height=12\nDrawBorders=false\n$X=getWidth($ParentControl) - 4\n$Y=0\n\n[winbar_lr]\nBackgroundTexture=bar_lr.png\n$Width=24\n$Height=12\nDrawBorders=false\n$X=getWidth($ParentControl) - 4\n$Y=getHeight($ParentControl) - getHeight($Self)\n\n[winbar_ll]\nBackgroundTexture=bar_ll.png\n$Width=24\n$Height=12\nDrawBorders=false\n$X=- (getWidth($Self) - 4)\n$Y=getHeight($ParentControl) - getHeight($Self)\n\n[winbar_r]\nBackgroundTexture=rightbar.png\nDrawMode=Tiled\n$Width=24\n$Height=getHeight($ParentControl) - (getHeight(winbar_ur) + getHeight(winbar_lr))\nDrawBorders=false\n$X=getWidth($ParentControl) - 4\n$Y=getHeight(winbar_ur)\n\n[winbar_l]\nBackgroundTexture=leftbar.png\nDrawMode=Tiled\n$Width=24\n$Height=getHeight($ParentControl) - (getHeight(winbar_ul) + getHeight(winbar_ll))\nDrawBorders=false\n$X=- (getWidth($Self) - 4)\n$Y=getHeight(winbar_ul)\n\n[glow_top_left]\nBackgroundTexture=glow_tl.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=16\nDrawBorders=false\n$X=4\n$Y=0\n\n[glow_top_right]\nBackgroundTexture=glow_tr.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=16\nDrawBorders=false\n$X=4\n$X=getWidth($ParentControl) - getWidth($Self) - 4\n\n[glow_bottom_left]\nBackgroundTexture=glow_bl.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=16\nDrawBorders=false\n$X=4\n$Y=getHeight($ParentControl) - getHeight($Self)\n\n[glow_bottom_right]\nBackgroundTexture=glow_br.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=16\nDrawBorders=false\n$X=getWidth($ParentControl) - getWidth($Self) - 4\n$Y=getHeight($ParentControl) - getHeight($Self)\n\n[glow_top]\nBackgroundTexture=glow_t.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=getWidth($ParentControl) - (getWidth(glow_top_left) + getWidth(glow_top_right)) - 8\n$Height=16\nDrawBorders=false\n$X=getWidth(glow_top_left) + 4\n$Y=0\n\n[glow_bottom]\nBackgroundTexture=glow_b.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=getWidth($ParentControl) - (getWidth(glow_bottom_left) + getWidth(glow_bottom_right)) - 8\n$Height=16\nDrawBorders=false\n$X=getWidth(glow_top_left) + 4\n$Y=getHeight($ParentControl) - getHeight($Self)\n\n[glow_left]\nBackgroundTexture=glow_l.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=getHeight($ParentControl) - (getHeight(glow_top_left) + getHeight(glow_bottom_left))\nDrawBorders=false\n$X=4\n$Y=getHeight(glow_top_left)\n\n[glow_right]\nBackgroundTexture=glow_r.png\nRemapColor=192,192,192,192\t;extra transparency\n$Width=16\n$Height=getHeight($ParentControl) - (getHeight(glow_top_right) + getHeight(glow_bottom_right))\nDrawBorders=false\n$X=getWidth($ParentControl) - getWidth($Self) - 4\n$Y=getHeight(glow_top_right)\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/KeyboardCommands.ini",
    "content": "; Dawn of the Tiberium Age (DTA) CnCNet Client\n; In-Game Keyboard Commands\n; https://github.com/CnCNet/xna-cncnet-client\n; If you use or redistribute the client in any public projects, please include\n; Rampastring and Dawn of the Tiberium Age in your project's credits.\n\n[ChatToAllies]\nUIName=Chat to allies\nCategory=Multiplayer\nDescription=Chat to players in your team.\nDefaultKey=8\n\n[ChatToAll]\nUIName=Chat to everyone\nCategory=Multiplayer\nDescription=Chat to all players in the game (same as F8).\nDefaultKey=13\n\n[GrantControl]\nUIName=Grant Control\nCategory=Control\nDescription=Give control of your units to the owner of a selected object.\nDefaultKey=0\n\n[SelectOneLess]\nUIName=Select One Unit Less\nCategory=Control\nDescription=Randomly unselect one of your selected units.\nDefaultKey=0\n\n[ToggleRadar]\nUIName=Radar Toggle\nCategory=Interface\nDescription=Toggle between the radar and the kill count screen (multiplayer only).\nDefaultKey=9\n\n[ScreenCapture]\nUIName=Screen Capture\nCategory=Interface\nDescription=Takes a screenshot and saves it to the \"Screenshots\" sub-directory in your game directory.\nDefaultKey=579\n\n[ToggleInfoPanel]\nUIName=Toggle Info Panel\nCategory=Sidebar\t;was Interface\nDescription=Toggles the state of the sidebar info panel.\nDefaultKey=192\n\n[ShowHelp]\nUIName=Show Key Commands\nCategory=Interface\nDescription=Displays an overview of important keyboard commands.\nDefaultKey=20\n\n[PlaceBuilding]\nUIName=Place Building\nCategory=Interface\nDescription=Places a finished building.\nDefaultKey=90\n\n[RepeatBuilding]\nUIName=Repeat Last Building\nCategory=Interface\nDescription=Repeats the last finished building.\nDefaultKey=602\n\n[TogglePower]\nUIName=Power Mode\nCategory=Interface\nDescription=Enable power mode (allows powering structures on and off).\nDefaultKey=80\n\n[ToggleRepair]\nUIName=Repair Mode\nCategory=Interface\nDescription=Enable repair mode.\nDefaultKey=82\n\n[ToggleSell]\nUIName=Sell Mode\nCategory=Interface\nDescription=Enable sell mode.\nDefaultKey=594\n\n[WaypointMode]\nUIName=Waypoint Mode\nCategory=Interface\nDescription=Enable waypoint mode.\nDefaultKey=87\n\n[DeleteWaypoint]\nUIName=Delete Waypoint\nCategory=Interface\nDescription=Deletes a waypoint.\nDefaultKey=110\n\n[ScrollNorth]\nUIName=Scroll North\nCategory=Interface\nDescription=Scroll the camera towards the north.\nDefaultKey=2086\n\n[ScrollSouth]\nUIName=Scroll South\nCategory=Interface\nDescription=Scroll the camera towards the south.\nDefaultKey=2088\n\n[ScrollEast]\nUIName=Scroll East\nCategory=Interface\nDescription=Scroll the camera towards the east.\nDefaultKey=2087\n\n[ScrollWest]\nUIName=Scroll West\nCategory=Interface\nDescription=Scroll the camera towards the west.\nDefaultKey=2085\n\n[LeftSidebarUp]\nUIName=Structure List Up\nCategory=Sidebar\nDescription=Scroll the sidebar's structure list up.\nDefaultKey=36\n\n[RightSidebarUp]\nUIName=Unit List Up\nCategory=Sidebar\nDescription=Scroll the sidebar's unit list up.\nDefaultKey=33\n\n[SidebarPageUp]\nUIName=Sidebar Page Up\nCategory=Sidebar\nDescription=Scroll the sidebar up by a page.\nDefaultKey=0\n\n[LeftSidebarPageUp]\nUIName=Structure List Page Up\nCategory=Sidebar\nDescription=Scroll the sidebar's structure list up by a page.\nDefaultKey=0\n\n[RightSidebarPageUp]\nUIName=Unit List Page Up\nCategory=Sidebar\nDescription=Scroll the sidebar's unit list up by a page.\nDefaultKey=0\n\n[LeftSidebarDown]\nUIName=Structure List Down\nCategory=Sidebar\nDescription=Scroll the sidebar's structure list down.\nDefaultKey=35\n\n[RightSidebarDown]\nUIName=Unit List Down\nCategory=Sidebar\nDescription=Scroll the sidebar's unit list down.\nDefaultKey=34\n\n[SidebarPageDown]\nUIName=Sidebar Page Down\nCategory=Sidebar\nDescription=Scroll the sidebar down by a page.\nDefaultKey=0\n\n[LeftSidebarPageDown]\nUIName=Structure List Page Down\nCategory=Sidebar\nDescription=Scroll the sidebar's structure list down by a page.\nDefaultKey=0\n\n[RightSidebarPageDown]\nUIName=Unit List Page Down\nCategory=Sidebar\nDescription=Scroll the sidebar's unit list down by a page.\nDefaultKey=0\n\n[SidebarUp]\nUIName=Sidebar Up\nCategory=Sidebar\nDescription=Scroll the sidebar up.\nDefaultKey=0\n\n[SidebarDown]\nUIName=Sidebar Down\nCategory=Sidebar\nDescription=Scroll the sidebar down.\nDefaultKey=0\n\n[SelectType]\nUIName=Select Same Type\nCategory=Selection\nDescription=Select all units on the screen that are the type of your currently selected units.\nDefaultKey=84\n\n[SelectView]\nUIName=Select View\nCategory=Selection\nDescription=Select all units on the screen.\nDefaultKey=69\n\n[NextObject]\nUIName=Next Unit\nCategory=Selection\nDescription=Select the next unit.\nDefaultKey=78\n\n[PreviousObject]\nUIName=Previous Unit\nCategory=Selection\nDescription=Select the previous unit.\nDefaultKey=66\n\n[CenterView]\nUIName=Center View\nCategory=Interface\nDescription=Center the camera to the selected objects.\nDefaultKey=12\n\n[Options]\nUIName=Options Menu\nCategory=Interface\nDescription=Open the in-game Options menu.\nDefaultKey=27\n\n[CenterBase]\nUIName=Center Base\nCategory=Interface\nDescription=Center the camera on your base.\nDefaultKey=72\n\n[Follow]\nUIName=Follow\nCategory=Interface\nDescription=Make the selected objects follow another object.\nDefaultKey=70\n\n[View1]\nUIName=View Bookmark 1\nCategory=Interface\nDescription=Center the camera on bookmark 1.\nDefaultKey=120\n\n[View2]\nUIName=View Bookmark 2\nCategory=Interface\nDescription=Center the camera on bookmark 2.\nDefaultKey=121\n\n[View3]\nUIName=View Bookmark 3\nCategory=Interface\nDescription=Center the camera on bookmark 3.\nDefaultKey=122\n\n[View4]\nUIName=View Bookmark 4\nCategory=Interface\nDescription=Center the camera on bookmark 4.\nDefaultKey=123\n\n[SetView1]\nUIName=Set Bookmark 1\nCategory=Interface\nDescription=Sets bookmark 1.\nDefaultKey=632\n\n[SetView2]\nUIName=Set Bookmark 2\nCategory=Interface\nDescription=Sets bookmark 2.\nDefaultKey=633\n\n[SetView3]\nUIName=Set Bookmark 3\nCategory=Interface\nDescription=Sets bookmark 3.\nDefaultKey=634\n\n[SetView4]\nUIName=Set Bookmark 4\nCategory=Interface\nDescription=Sets bookmark 4.\nDefaultKey=635\n\n[CenterOnRadarEvent]\nUIName=Goto Radar Event\nCategory=Interface\nDescription=Center the camera around the latest radar event.\nDefaultKey=32\n\n[ToggleAlliance]\nUIName=Alliance\nCategory=Control\nDescription=Form an alliance with the owner of a selected object.\nDefaultKey=65\n\n[DeployObject]\nUIName=Deploy Object\nCategory=Control\nDescription=Deploy selected units.\nDefaultKey=68\n\n[GuardObject]\nUIName=Guard\nCategory=Control\nDescription=Make your selected units guard the nearby area and automatically attack enemies.\nDefaultKey=71\n\n[ScatterObject]\nUIName=Scatter\nCategory=Control\nDescription=Make your selected units scatter.\nDefaultKey=88\n\n[StopObject]\nUIName=Stop Object\nCategory=Control\nDescription=Stop your selected units.\nDefaultKey=83\n\n[AllToCheer]\nUIName=Cheer\nCategory=Control\nDescription=Make all of your infantry units cheer.\nDefaultKey=0\n\n[TeamAddSelect_1]\nUIName=Add Select Team 1\nCategory=Team\nDescription=Select team 1 without unselecting already selected objects\nDefaultKey=305\n\n[TeamAddSelect_2]\nUIName=Add Select Team 2\nCategory=Team\nDescription=Select team 2 without unselecting already selected objects\nDefaultKey=306\n\n[TeamAddSelect_3]\nUIName=Add Select Team 3\nCategory=Team\nDescription=Select team 3 without unselecting already selected objects\nDefaultKey=307\n\n[TeamAddSelect_4]\nUIName=Add Select Team 4\nCategory=Team\nDescription=Select team 4 without unselecting already selected objects\nDefaultKey=308\n\n[TeamAddSelect_5]\nUIName=Add Select Team 5\nCategory=Team\nDescription=Select team 5 without unselecting already selected objects\nDefaultKey=309\n\n[TeamAddSelect_6]\nUIName=Add Select Team 6\nCategory=Team\nDescription=Select team 6 without unselecting already selected objects\nDefaultKey=310\n\n[TeamAddSelect_7]\nUIName=Add Select Team 7\nCategory=Team\nDescription=Select team 7 without unselecting already selected objects\nDefaultKey=311\n\n[TeamAddSelect_8]\nUIName=Add Select Team 8\nCategory=Team\nDescription=Select team 8 without unselecting already selected objects\nDefaultKey=312\n\n[TeamAddSelect_9]\nUIName=Add Select Team 9\nCategory=Team\nDescription=Select team 9 without unselecting already selected objects\nDefaultKey=313\n\n[TeamAddSelect_10]\nUIName=Add Select Team 10\nCategory=Team\nDescription=Select team 10 without unselecting already selected objects\nDefaultKey=304\n\n[TeamCenter_1]\nUIName=Center Team 1\nCategory=Team\nDescription=Center the camera around team 1\nDefaultKey=1073\n\n[TeamCenter_2]\nUIName=Center Team 2\nCategory=Team\nDescription=Center the camera around team 2\nDefaultKey=1074\n\n[TeamCenter_3]\nUIName=Center Team 3\nCategory=Team\nDescription=Center the camera around team 3\nDefaultKey=1075\n\n[TeamCenter_4]\nUIName=Center Team 4\nCategory=Team\nDescription=Center the camera around team 4\nDefaultKey=1076\n\n[TeamCenter_5]\nUIName=Center Team 5\nCategory=Team\nDescription=Center the camera around team 5\nDefaultKey=1077\n\n[TeamCenter_6]\nUIName=Center Team 6\nCategory=Team\nDescription=Center the camera around team 6\nDefaultKey=1078\n\n[TeamCenter_7]\nUIName=Center Team 7\nCategory=Team\nDescription=Center the camera around team 7\nDefaultKey=1079\n\n[TeamCenter_8]\nUIName=Center Team 8\nCategory=Team\nDescription=Center the camera around team 8\nDefaultKey=1080\n\n[TeamCenter_9]\nUIName=Center Team 9\nCategory=Team\nDescription=Center the camera around team 9\nDefaultKey=1081\n\n[TeamCenter_10]\nUIName=Center Team 10\nCategory=Team\nDescription=Center the camera around team 10\nDefaultKey=1072\n\n[TeamCreate_1]\nUIName=Create Team 1\nCategory=Team\nDescription=Creates team 1\nDefaultKey=561\n\n[TeamCreate_2]\nUIName=Create Team 2\nCategory=Team\nDescription=Creates team 2\nDefaultKey=562\n\n[TeamCreate_3]\nUIName=Create Team 3\nCategory=Team\nDescription=Creates team 3\nDefaultKey=563\n\n[TeamCreate_4]\nUIName=Create Team 4\nCategory=Team\nDescription=Creates team 4\nDefaultKey=564\n\n[TeamCreate_5]\nUIName=Create Team 5\nCategory=Team\nDescription=Creates team 5\nDefaultKey=565\n\n[TeamCreate_6]\nUIName=Create Team 6\nCategory=Team\nDescription=Creates team 6\nDefaultKey=566\n\n[TeamCreate_7]\nUIName=Create Team 7\nCategory=Team\nDescription=Creates team 7\nDefaultKey=567\n\n[TeamCreate_8]\nUIName=Create Team 8\nCategory=Team\nDescription=Creates team 8\nDefaultKey=568\n\n[TeamCreate_9]\nUIName=Create Team 9\nCategory=Team\nDescription=Creates team 9\nDefaultKey=569\n\n[TeamCreate_10]\nUIName=Create Team 10\nCategory=Team\nDescription=Creates team 10\nDefaultKey=560\n\n[TeamSelect_1]\nUIName=Select Team 1\nCategory=Team\nDescription=Selects team 1\nDefaultKey=49\n\n[TeamSelect_2]\nUIName=Select Team 2\nCategory=Team\nDescription=Selects team 2\nDefaultKey=50\n\n[TeamSelect_3]\nUIName=Select Team 3\nCategory=Team\nDescription=Selects team 3\nDefaultKey=51\n\n[TeamSelect_4]\nUIName=Select Team 4\nCategory=Team\nDescription=Selects team 4\nDefaultKey=52\n\n[TeamSelect_5]\nUIName=Select Team 5\nCategory=Team\nDescription=Selects team 5\nDefaultKey=53\n\n[TeamSelect_6]\nUIName=Select Team 6\nCategory=Team\nDescription=Selects team 6\nDefaultKey=54\n\n[TeamSelect_7]\nUIName=Select Team 7\nCategory=Team\nDescription=Selects team 7\nDefaultKey=55\n\n[TeamSelect_8]\nUIName=Select Team 8\nCategory=Team\nDescription=Selects team 8\nDefaultKey=56\n\n[TeamSelect_9]\nUIName=Select Team 9\nCategory=Team\nDescription=Selects team 9\nDefaultKey=57\n\n[TeamSelect_10]\nUIName=Select Team 10\nCategory=Team\nDescription=Selects team 10\nDefaultKey=48\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/LANGameLobby.ini",
    "content": "[INISystem]\nBasedOn=MultiplayerGameLobby.ini\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/LANLobby.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[lblColor]\nLocation=300,14\n\n[ddColor]\n;Location=395,12\n\n[lblCurrentChannel]\nDistanceFromRightBorder=431\n\n[ddCurrentChannel]\nDistanceFromRightBorder=212\n\n[lbGameList]\nLocation=12,43\nFillHeight=45\n\n[lbPlayerList]\nLocation=0,43\nDistanceFromRightBorder=12\nFillHeight=45\n\n[lbChatMessages]\nLocation=300,43\nFillWidth=212\nFillHeight=45\n\n[tbChatInput]\nDistanceFromBottomBorder=12\nFillWidth=212\n\n[btnLogout]\nDistanceFromRightBorder=12\nDistanceFromBottomBorder=12\n\n[btnNewGame]\nDistanceFromBottomBorder=12\n\n[btnJoinGame]\nDistanceFromBottomBorder=12\n\n[ExtraControls]\nL1=btnModDB:XNALinkButton\nL2=btnForums:XNALinkButton\n\n[btnModDB]\nIdleTexture=moddbInactive.png\nHoverTexture=moddbActive.png\nURL=http://www.moddb.com/mods/tiberian-sun-client\nSize=21,21\nLocation=0,12\nDistanceFromRightBorder=180\nDrawOrder=1\n\n[btnForums]\nIdleTexture=forumsInactive.png\nHoverTexture=forumsActive.png\nURL=https://ppmforums.com/index.php?f=24\nSize=21,21\nLocation=0,12\nDistanceFromRightBorder=159\nDrawOrder=1\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/LoadingScreen.ini",
    "content": "[LoadingScreen]\nSize=1280,720\nBackgroundTexture=lsbg.png\nDrawBorders=false\n\n[ExtraControls]\n0=logo:XNAExtraPanel\n1=text:XNAExtraPanel\n2=legal:XNAExtraPanel\n\n[logo]\nLocation=754,15\nBackgroundTexture=lslogo.png\nRepeatingImage=false\nSize=523,318\n\n[text]\nLocation=329,576\nBackgroundTexture=lstext.png\nRepeatingImage=false\nSize=198,32\n\n[legal]\nBackgroundTexture=ts_legal_text.png\nRepeatingImage=false\nSize=815,114\nDistanceFromRightBorder=-9\nDistanceFromBottomBorder=-10\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/MainMenu.ini",
    "content": "[MainMenu]\nSize=1280,720\nDrawBorders=false\n\n[MainMenuUIPanel]\nDrawBorders=false\n\n[ExtraControls]\n0=Logo:XNAExtraPanel\n1=txtVersion:XNALabel\n2=btnRankedMatch:XNALinkButton\n\n[Logo]\nLocation=369,8\nBackgroundTexture=MainMenu/Logo.png\n\n[btnNewCampaign]\nLocation=490,261\nIdleTexture=MainMenu/campaign.png\n\n[btnLoadGame]\nLocation=490,315\nIdleTexture=MainMenu/loadmission.png\n\n[btnCnCNet]\nLocation=490,369\nIdleTexture=MainMenu/playonline.png\nHoverTexture=MainMenu/playonline_c.png\n\n[btnRankedMatch]\nLocation=490,423\nIdleTexture=MainMenu/rankedmatch.png\nHoverTexture=MainMenu/rankedmatch_c.png\nHoverSoundEffect=MainMenu/button.wav\nURL=CnCNetQM.exe\nEnabled=false\n\n[btnLan]\nLocation=490,477\nIdleTexture=MainMenu/lan.png\n\n[btnSkirmish]\nLocation=490,531\nIdleTexture=MainMenu/skirmish.png\n\n[btnExtras]\nLocation=490,531\nVisible=false\nIdleTexture=MainMenu/extras.png\n\n[btnOptions]\nLocation=490,585\nIdleTexture=MainMenu/options.png\n\n[btnExit]\nLocation=145,635\nIdleTexture=MainMenu/exitgame.png\n\n[btnCredits]\nLocation=565,668\nIdleTexture=MainMenu/credits.png\nHoverTexture=MainMenu/credits_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnStatistics]\nLocation=415,668\nIdleTexture=MainMenu/statistics.png\nHoverTexture=MainMenu/statistics_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\n\n[btnMapEditor]\nLocation=715,668\nIdleTexture=MainMenu/mapeditor.png\nHoverTexture=MainMenu/mapeditor_c.png\nText=\nHoverSoundEffect=MainMenu/button.wav\nEnabled=false\n\n[lblCnCNetStatus]\nText=TS Players on CnCNet\nRemapColor=45,228,255\nLocation=1099,70\nDistanceFromRightBorder=159\n\n[lblCnCNetPlayerCount]\nRemapColor=45,228,255\nLocation=990,70\nDistanceFromRightBorder=296\n\n[txtVersion]\nText=Version:\nRemapColor=45,228,255\nLocation=990,91\nDistanceFromRightBorder=262\n\n[lblVersion]\nRemapColor=45,228,255\nIdleColor=45,228,255\nHoverColor=255,255,255\nLocation=1304,91\nDistanceFromRightBorder=256\n\n[lblUpdateStatus]\nRemapColor=45,228,255\nIdleColor=45,228,255\nHoverColor=255,255,255\nLocation=990,112\nDistanceFromRightBorder=139\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/MultiplayerGameLobby.ini",
    "content": "[INISystem]\nBasedOn=GameLobbyBase.ini\n\n[GameOptionsPanel]\n$CC18=chkIngameAllying:GameLobbyCheckBox\n\n[MultiplayerGameLobby]\n$BaseSection=SkirmishLobby\nPlayerStatusIndicatorX=3\nPlayerStatusIndicatorY=1\nPlayerOptionLocationX=22    ;def=25\nPlayerNameWidth=117; def=136\n$CCMP01=btnLockGame:XNAClientButton\n$CCMP02=lbChatMessages:ChatListBox\n$CCMP03=lbChatMessages_Player:ChatListBox\n$CCMP04=tbChatInput:XNAChatTextBox\n$CCMP05=tbChatInput_Player:XNAChatTextBox\n$CCMP06=chkAutoSave:GameLobbyCheckBox\n$CCMP07=chkAutoReady:XNAClientCheckBox\n\n\n[cmbGameSpeedCap]\nItems=60 FPS,52 FPS,45 FPS,40 FPS,30 FPS,20 FPS,15 FPS\nDefaultIndex=0\n\n[btnLockGame]\nText=Lock Game\n$X=getRight(btnLaunchGame) + BUTTON_SPACING\n$Y=getY(btnLaunchGame)\n$Width=133\n\n[btnLeaveGame]\nText=Leave Game\n\n[chkAutoSave]\nText=Auto Save\n$X=getRight(btnLockGame) + BUTTON_SPACING\n$Y=getY(btnLaunchGame) + 3\n\n[chkAutoReady]\nText=Auto Accept\n$X=getRight(chkAutoSave) + BUTTON_SPACING\n$Y=getY(btnLaunchGame) + 3\n\n[lbMapList]\n$Height=getHeight(MapPreviewBox)\n$Y=getY(MapPreviewBox)\n\n[lbChatMessages]\nSolidColorBackgroundTexture=0,0,0,192\n$X=EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP\n$Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING)\n$Height=getHeight(GameOptionsPanel) - 21 - LOBBY_PANEL_SPACING\t;235\n\n[lbChatMessages_Player]\nBaseSection=lbChatMessages\n$Height=getBottom(MapPreviewBox) - getY($Self) - 21 - LOBBY_PANEL_SPACING\n\n[tbChatInput]\nSuggestion=Type here to chat...\n$Width=getWidth(lbChatMessages)\n$Height=21\n$X=getX(lbChatMessages)\n$Y=getBottom(lbChatMessages) + LOBBY_PANEL_SPACING\n\n[tbChatInput_Player]\nSuggestion=Type here to chat...\n$Width=getWidth(lbChatMessages_Player)\n$Height=21\n$X=getX(lbChatMessages_Player)\n$Y=getBottom(lbChatMessages_Player) + LOBBY_PANEL_SPACING\n\n[chkMultiEng]\nChecked=True\n\n[chkCrates]\nChecked=False\n\n[chkIngameAllying]\nEnabled=True"
  },
  {
    "path": "DXMainClient/Resources/DTA/OptionsWindow.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[DisplayOptionsPanelExtraControls]\n0=chkStretchMovies:SettingCheckBox\n1=chkMEDDraw:FileSettingCheckBox\n2=lblReShade:XNALabel\n3=ddReShade:FileSettingDropDown\n\n[chkMEDDraw]\nLocation=12,217 ;285,216\nText=Enable DDWrapper for Map Editor\nToolTip=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. \nEnabledFile0=Resources/Compatibility/DLL/ddwrapper.dll,Map Editor/ddraw32.dll,AlwaysOverwrite_LinkAsReadOnly\nEnabledFile1=Resources/Compatibility/Configs/aqrit.cfg,Map Editor/aqrit.cfg,KeepChanges\n\n[lblReShade]\nText=ReShade Shaders:\nToolTip=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.\nLocation=13,246\n\n[ddReShade]\nLocation=140,246 ;161\nSize=120,21 ;133,21\nItems=Disabled,Enabled - DX11,Enabled - DX9,Enabled - OpenGL\nToolTip=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.\nDefaultValue=0\nCheckFilePresence=yes\nResetUnselectableItem=yes\nForceApplyUnselectableItem=no\nRestartRequired=false\nItem1File0=Resources/ReShade Files/dxgi.dll,dxgi.dll,AlwaysOverwrite_LinkAsReadOnly\nItem1File1=Resources/ReShade Files/ReShade.ini,ReShade.ini\nItem2File0=Resources/ReShade Files/d3d9.dll,d3d9.dll,AlwaysOverwrite_LinkAsReadOnly\nItem2File1=Resources/ReShade Files/ReShade.ini,ReShade.ini\nItem3File0=Resources/ReShade Files/opengl32.dll,opengl32.dll,AlwaysOverwrite_LinkAsReadOnly\nItem3File1=Resources/ReShade Files/ReShade.ini,ReShade.ini\n\n[lblDetailLevel]\nToolTip=Select the level of detail. Lower levels will reduce visual effects and increase performance.\n\n[ddDetailLevel]\nToolTip=Select the level of detail. Lower levels will reduce visual effects and increase performance.\n\n[lblRenderer]\nToolTip=Select the DDraw wrapper to use. If you experience graphical or performance issues, try a different wrapper.\n\n[ddRenderer]\nToolTip=Select the DDraw wrapper to use. If you experience graphical or performance issues, try a different wrapper.\n\n[chkBackBufferInVRAM]\nText=Back Buffer in Video Memory  ;Here I moved the explanation to the tooltip\nToolTip=Enable back buffer in VRAM. Reduces performance, but is necessary on some systems.\n\n[chkScrollCoasting]\nToolTip=Enable smooth scrolling.\n\n[chkTargetLines]\nToolTip=Show lines between selected units and targets.@Green lines indicate movement, red lines attack.\n\n[chkTooltips]\nToolTip=Enable in-game tooltips.\n\n[chkBlackChatBackground]\nText=Dark Chat Background\nToolTip=Use black background for in-game chat messages.\n\n[chkAltToUndeploy]\nText=Hold Alt to Undeploy\nToolTip=Undeploy units by holding the [Alt] key while giving a move command.\n\n[chkStretchMovies]\nLocation=12,196\nText=Stretch Videos\nSettingSection=Video\nSettingKey=StretchMovies\n\n[chkStopMusicOnMenu]\nLocation=12,221\n\n[btnSave]\nDistanceFromRightBorder=124\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/ReShade Files/ReShade.ini",
    "content": "[D3D9]\nDepthCopyAtClearIndex=0\nDepthCopyBeforeClears=0\nDisableINTZ=0\nUseAspectRatioHeuristics=1\n\n[DX11_BUFFER_DETECTION]\nDepthBufferClearingNumber=0\nDepthBufferRetrievalMode=0\nUseAspectRatioHeuristics=1\n\n[DX9_BUFFER_DETECTION]\nDisableINTZ=0\nPreserveDepthBuffer=0\nPreserveDepthBufferIndex=4294967295\nUseAspectRatioHeuristics=1\n\n[GENERAL]\nClockFormat=0\nCurrentPresetPath=.\\reshade-shaders\\RotE_Xtended.ini\nEffectSearchPaths=.\\reshade-shaders\\Shaders,.\\reshade-shaders\\Shaders\\Pirate,.\\reshade-shaders\\Shaders\\qUINT,.\\reshade-shaders\\Shaders\\PD80,.\\reshade-shaders\\Shaders\\Depth3D,.\\reshade-shaders\\Shaders\\Daodan\nFPSPosition=1\nNewVariableUI=0\nNoDebugInfo=0\nNoFontScaling=1\nNoReloadOnInit=0\nPerformanceMode=1\nPreprocessorDefinitions=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\nPresetPath=.\\reshade-shaders\\RotE_Xtended.ini\nPresetTransitionDelay=1000\nSaveWindowState=0\nScreenshotFormat=1\nScreenshotIncludePreset=0\nScreenshotPath=\nScreenshotSaveBefore=0\nScreenshotSaveUI=0\nShowClock=0\nShowFPS=0\nShowFrameTime=0\nShowScreenshotMessage=1\nSkipLoadingDisabledEffects=0\nTextureSearchPaths=.\\reshade-shaders\\Textures\nTutorialProgress=4\n\n[INPUT]\nForceShortcutModifiers=1\nInputProcessing=2\nKeyEffects=0,0,0,0\nKeyMenu=36,0,0,0\nKeyNextPreset=0,0,0,0\nKeyOverlay=36,0,0,0\nKeyPerformanceMode=0,0,0,0\nKeyPreviousPreset=0,0,0,0\nKeyReload=0,0,0,0\nKeyScreenshot=44,0,0,0\n\n[OPENGL]\nForceMainDepthBuffer=0\nUseAspectRatioHeuristics=1\n\n[OVERLAY]\nClockFormat=0\nFPSPosition=1\nNoFontScaling=0\nSaveWindowState=0\nShowClock=0\nShowForceLoadEffectsButton=1\nShowFPS=0\nShowFrameTime=0\nShowScreenshotMessage=1\nTutorialProgress=2\nVariableListHeight=300.000000\nVariableListUseTabs=0\n\n[SCREENSHOTS]\nClearAlpha=1\nFileFormat=1\nFileNamingFormat=0\nJPEGQuality=90\nSaveBeforeShot=0\nSaveOverlayShot=0\nSavePath=\nSavePresetFile=0\n\n[STYLE]\nAlpha=1.000000\nChildRounding=0.000000\nColFPSText=1.000000,1.000000,0.784314,1.000000\nEditorFont=\nEditorFontSize=13\nEditorStyleIndex=0\nFont=\nFontSize=13\nFPSScale=1.000000\nFrameRounding=0.000000\nGrabRounding=0.000000\nPopupRounding=0.000000\nScrollbarRounding=0.000000\nStyleIndex=2\nTabRounding=4.000000\nWindowRounding=0.000000\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/Renderers.ini",
    "content": "; DTA CnCNet Client Renderers.ini\n; Specifies the available DirectDraw wrappers in the client's options menu.\n\n[Renderers]\n0=CnC-DDRAW\n1=DDrawCompat\n2=TS-DDRAW-OPENGL\n3=TS-DDRAW-GDI\n4=Software\n5=Default\n\n; Specifies the default renderers for different operating systems.\n[DefaultRenderer]\nUNKNOWN=Default\nWINXP=Default\nWINVISTA=CnC-DDRAW\nWIN7=CnC-DDRAW\nWIN810=CnC-DDRAW\nUNIX=Default\n\n; Renderer sections start below.\n\n; The main ddraw.dll for a renderer is specified in DLLName=. \n; The file is expected to be found from the Resources\\ directory, and it is\n; copied to the game directory as ddraw.dll when settings are saved.\n\n; AdditionalFiles= is a comma-separated list of additional files to be copied\n; to the game directory. The client also expects to find them from the Resources\\\n; directory, and copies them to the main directory when settings are saved. \n\n; ConfigFilePath= works similarly. The only difference is that if the config\n; file already exists, it is not overwritten (the DLLs and additional files are).\n\n; You can also specify sub-directories in the Resources\\ directory for the paths.\n; For example, if you specify DLLName=Renderers\\my_awesome_wrapper.dll, the client\n; expects to find the file from \\Resources\\Renderers\\my_awesome_wrapper.dll.\n; When settings are saved, it is still copied to the root of the main game directory.\n\n[Default]\nUIName=Stock\n\n[TS-DDRAW-OPENGL]\nUIName=TS-DDRAW (OGL)\nDLLName=ts_ddraw.dll\t;ts-ddraw-opengl.dll\nResConfigFileName=ts-ddraw.ini\nConfigFileName=ddraw.ini\nUseQres=No\nSingleCoreAffinity=false\n\n[TS-DDRAW-GDI]\nUIName=TS-DDRAW (GDI)\nDLLName=ts_ddraw.dll\t;ts-ddraw-gdi.dll\nResConfigFileName=ts-ddraw-gdi.ini\nConfigFileName=ddraw.ini\nUseQres=No\nSingleCoreAffinity=false\n\n[CnC-DDRAW]\nUIName=CnC-DDRAW\nDLLName=cnc-ddraw.dll\nResConfigFileName=cnc-ddraw.ini\nConfigFileName=ddraw.ini\nUseQres=No\nWindowedModeSection=ddraw\nWindowedModeKey=windowed\nBorderlessWindowedModeKey=border\nIsBorderlessWindowedModeKeyReversed=true\n\n[Software]\nUIName=Software\nDLLName=ddraw_nohw.dll\nDisallowedOperatingSystems=WINVISTA,WIN7,WIN810\n\n[DDrawCompat]\nUIName=DDrawCompat\nDLLName=ddrawcompat.dll\nResConfigFileName=ddrawcompat.ini\nConfigFileName=ddraw.ini\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/SkirmishLobby.ini",
    "content": "[INISystem]\nBasedOn=GameLobbyBase.ini\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/StatisticsWindow.ini",
    "content": "[INISystem]\nBasedOn=GenericWindow.ini\n\n[panelGameStatistics]\nSolidColorBackgroundTexture=0,0,0,32\n\n[panelTotalStatistics]\nSolidColorBackgroundTexture=0,8,0,128\n\n[btnReturnToMenu]\nDistanceFromRightBorder=12\n\n[btnClearStatistics]\nVisible=true\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/UpdaterConfig.ini",
    "content": "[Settings]\n; Comma-separated list of filename masks that are exempted from the file integrity checks. \n; Overrides a following built-in list: .rtf,.txt,Theme.ini,gui_settings.xml\nIgnoreMasks=.rtf, .txt, Theme.ini, gui_settings.xml, .htm, html, .doc, .xdoc, .log\n\n; List of update download mirrors. \n; Format: URL,UI Name,Location. \n; Example: 0=http://testurl/updates/,Test Update Server,Europe\n; Location is optional.\n[DownloadMirrors]\n0=https://downloads.cncnet.org/updates/games/ts/live/,CnCNet,Europe\n\n; List of custom components.\n; Format: UI Name, Version File ID, Download File Path, Local File Path\n; Example: 0=Test Component, TESTCOMPONENT, Test/testfile.mix, Test\\testfile.mix\n[CustomComponents]\n0=Tiberian Sun Music,TSMUSIC,MIX/SCORES.MIX,MIX\\SCORES.MIX\n1=Firestorm Music,FSMUSIC,MIX/SCORES01.MIX,MIX\\SCORES01.MIX\n2=Tiberian Sun GDI Campaign Videos,MOVIES01,MIX/Movies01.mix,MIX\\Movies01.mix\n3=Tiberian Sun Nod Campaign Videos,MOVIES02,MIX/Movies02.mix,MIX\\Movies02.mix\n4=Firestorm Campaign Videos,MOVIES03,MIX/Movies03.mix,MIX\\Movies03.mix\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/UserDefaults.ini",
    "content": "[Video]\nIntegerScaledClient=True\nBorderlessWindowedClient=False\n\n[Audio]\nPlayMainMenuMusic=False\n\n[Options]\nWriteInstallationPathToRegistry=False\nCheckforUpdates=False"
  },
  {
    "path": "DXMainClient/Resources/DTA/ZLIB.License.txt",
    "content": "\nThe following licenses govern use of the accompanying software, the\nDotNetZip library (\"the software\"). If you use the software, you accept\nthese licenses. If you do not accept the license, do not use the software.\n\nThe managed ZLIB code included in Ionic.Zlib.dll and Ionic.Zip.dll is\nmodified code, based on jzlib.\n\n\n\nThe following notice applies to jzlib:\n-----------------------------------------------------------------------\n\nCopyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice,\nthis list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in\nthe documentation and/or other materials provided with the distribution.\n\n3. The names of the authors may not be used to endorse or promote products\nderived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,\nINC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,\nOR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n-----------------------------------------------------------------------\n\njzlib is based on zlib-1.1.3.\n\nThe following notice applies to zlib:\n\n-----------------------------------------------------------------------\n\nCopyright (C) 1995-2004 Jean-loup Gailly and Mark Adler\n\n  The ZLIB software is provided 'as-is', without any express or implied\n  warranty.  In no event will the authors be held liable for any damages\n  arising from the use of this software.\n\n  Permission is granted to anyone to use this software for any purpose,\n  including commercial applications, and to alter it and redistribute it\n  freely, subject to the following restrictions:\n\n  1. The origin of this software must not be misrepresented; you must not\n     claim that you wrote the original software. If you use this software\n     in a product, an acknowledgment in the product documentation would be\n     appreciated but is not required.\n  2. Altered source versions must be plainly marked as such, and must not be\n     misrepresented as being the original software.\n  3. This notice may not be removed or altered from any source distribution.\n\n  Jean-loup Gailly jloup@gzip.org\n  Mark Adler madler@alumni.caltech.edu\n\n\n-----------------------------------------------------------------------\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/ZLIB.Ms-PL.txt",
    "content": "Microsoft Public License (Ms-PL)\n\nThis 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.\n\n1. Definitions\n\nThe terms \"reproduce,\" \"reproduction,\" \"derivative works,\" and \"distribution\" have the same meaning here as under U.S. copyright law.\n\nA \"contribution\" is the original software, or any additions or changes to the software.\n\nA \"contributor\" is any person that distributes its contribution under this license.\n\n\"Licensed patents\" are a contributor's patent claims that read directly on its contribution.\n\n2. Grant of Rights\n\n(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.\n\n(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.\n\n3. Conditions and Limitations\n\n(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.\n\n(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.\n\n(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.\n\n(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.\n\n(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.\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/cnc-ddraw.ini",
    "content": "; cnc-ddraw - https://github.com/FunkyFr3sh/cnc-ddraw\n\n[ddraw]\n; ### Optional settings ###\n; Use the following settings to adjust the look and feel to your liking\n\n\n; Stretch to custom resolution, 0 = defaults to the size game requests\nwidth=0\nheight=0\n\n; Override the width/height settings shown above and always stretch to fullscreen\n; Note: Can be combined with 'windowed=true' to get windowed-fullscreen aka borderless mode\nfullscreen=false\n\n; Run in windowed mode rather than going fullscreen\nwindowed=false\n\n; Maintain aspect ratio\nmaintas=false\n\n; Windowboxing / Integer Scaling\nboxing=false\n\n; Real rendering rate, -1 = screen rate, 0 = unlimited, n = cap\n; Note: Does not have an impact on the game speed, to limit your game speed use 'maxgameticks='\nmaxfps=-1\n\n; Vertical synchronization, enable if you get tearing - (Requires 'renderer=auto/opengl*/direct3d9*')\n; Note: vsync=true can fix tearing but it will cause input lag\nvsync=false\n\n; Automatic mouse sensitivity scaling\n; Note: Only works if stretching is enabled. Sensitivity will be adjusted according to the size of the window\nadjmouse=true\n\n; Preliminary libretro shader support - (Requires 'renderer=opengl*') https://github.com/libretro/glsl-shaders\n; 2x scaling example: https://imgur.com/a/kxsM1oY - 4x scaling example: https://imgur.com/a/wjrhpFV\n; You can specify a full path to a .glsl shader file here or use one of the values listed below\n; Possible values: Nearest neighbor, Bilinear, Bicubic, Lanczos, xBR-lv2\nshader=Resources\\Shaders\\interpolation\\catmull-rom-bilinear.glsl\n\n; Window position, -32000 = center to screen\nposX=-32000\nposY=-32000\n\n; Renderer, possible values: auto, opengl, openglcore, gdi, direct3d9, direct3d9on12 (auto = try direct3d9/opengl, fallback = gdi)\nrenderer=auto\n\n; Developer mode (don't lock the cursor)\ndevmode=false\n\n; Show window borders in windowed mode\nborder=true\n\n; Save window position/size/state on game exit and restore it automatically on next game start\n; Possible values: 0 = disabled, 1 = save to global 'ddraw' section, 2 = save to game specific section\nsavesettings=1\n\n; Should the window be resizable by the user in windowed mode?\nresizable=true\n\n; Upscaling filter for the direct3d9* renderers\n; Possible values: 0 = nearest-neighbor, 1 = bilinear, 2 = bicubic, 3 = lanczos (bicubic/lanczos only support 16/32bit color depth games)\nd3d9_filter=2\n\n; Enable upscale hack for high resolution patches (Supports C&C1, Red Alert 1 and KKND Xtreme)\nvhack=false\n\n; Switch between windowed/borderless modes with alt+enter rather than windowed/fullscreen modes\ntoggle_borderless=false\n\n; Switch between windowed/fullscreen upscaled modes with alt+enter rather than windowed/fullscreen modes\ntoggle_upscaled=false\n\n\n\n; ### Compatibility settings ###\n; Use the following settings in case there are any issues with the game\n\n\n; Hide WM_ACTIVATEAPP and WM_NCACTIVATE messages to prevent problems on alt+tab\nnoactivateapp=true\n\n; Max game ticks per second, possible values: -1 = disabled, -2 = refresh rate, 0 = emulate 60hz vblank, 1-1000 = custom game speed\n; Note: Can be used to slow down a too fast running game, fix flickering or too fast animations\n; Note: Usually one of the following values will work: 60 / 30 / 25 / 20 / 15 (lower value = slower game speed)\nmaxgameticks=0\n\n; Windows API Hooking, Possible values: 0 = disabled, 1 = IAT Hooking, 2 = Microsoft Detours, 3 = IAT+Detours Hooking (All Modules), 4 = IAT Hooking (All Modules)\n; Note: Change this value if windowed mode or upscaling isn't working properly\n; Note: 'hook=2' will usually work for problematic games, but 'hook=2' should be combined with renderer=gdi\nhook=4\n\n; Force minimum FPS, possible values: 0 = disabled, -1 = use 'maxfps=' value, -2 = same as -1 but force full redraw, 1-1000 = custom FPS\n; 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)\nminfps=-1\n\n; Disable fullscreen-exclusive mode for the direct3d9*/opengl* renderers\n; Note: Can be used in case some GUI elements like buttons/textboxes/videos/etc.. are invisible\nnonexclusive=false\n\n; Force CPU0 affinity, avoids crashes/freezing, *might* have a performance impact\n; Note: Disable this if the game is not running smooth or there are sound issues\nsinglecpu=true\n\n; Available resolutions, possible values: 0 = Small list, 1 = Very small list, 2 = Full list\n; Note: Set this to 2 if your chosen resolution is not working or does not show up in the list\n; Note: Set this to 1 if the game is crashing on startup\nresolutions=0\n\n; Child window handling, possible values: 0 = Disabled, 1 = Display top left, 2 = Display top left + repaint, 3 = Hide\n; Note: Disables upscaling if a child window was detected (to ensure the game is fully playable, may look weird though)\nfixchilds=2\n\n; Enable one of the following settings if your cursor doesn't work properly when upscaling is enabled\nhook_peekmessage=false\nhook_getmessage=false\n\n\n; Undocumented settings - You may or may not change these (You should rather focus on the settings above)\ntshack=true\nreleasealt=false\ngame_handles_close=false\nfixnotresponding=false\nguard_lines=200\nmax_resolutions=0\nlimit_bltfast=false\nlock_surfaces=false\nallow_wmactivate=false\nflipclear=false\nfixmousehook=false\nrgb555=false\nno_dinput_hook=false\nrefresh_rate=0\nanti_aliased_fonts_min_size=13\ncustom_width=0\ncustom_height=0\nmin_font_size=0\ndirect3d_passthrough=false\n\n\n\n; ### Hotkeys ###\n; Use the following settings to configure your hotkeys, 0x00 = disabled\n; Virtual-Key Codes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes\n\n\n; Switch between windowed and fullscreen mode = [Alt] + [Enter]\nkeytogglefullscreen=0x0D\n\n; Maximize window without frame = [Alt] + [Page Down]\nkeytogglemaximize=0x22\n\n; Unlock cursor 1 = [Ctrl] + [Tab]\nkeyunlockcursor1=0x09\n\n; Unlock cursor 2 = [Right Alt] + [Right Ctrl]\nkeyunlockcursor2=0xA3\n\n; Screenshot = [Prnt Scrn]\nkeyscreenshot=0x2C\n\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/ddrawcompat.ini",
    "content": "ColorKeyMethod = auto\nCpuAffinity = 1\nDisplayFilter = point\nDpiAwareness = permonitor\nFpsLimiter = msgloop(120)\nRenderColorDepth = app\nSupportedDepthFormats = 16\nSupportedResolutions = 640x400, native\nVSync = off\nWinVersionLie = 98\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/qres license.txt",
    "content": "QRes Source Code - Open Source License\n--------------------------------------\n \nCopyright (c) 1997-2005 by Berend Engelbrecht. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n \n- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n \n- 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.\n \n- 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. \n \nTHIS 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.\n\nBSD License template Copyright (c) 2005 by the Open Source Initiative\nhttp://www.opensource.org/licenses/bsd-license.php"
  },
  {
    "path": "DXMainClient/Resources/DTA/ts-ddraw-gdi.ini",
    "content": "; ts-ddraw - https://github.com/CnCNet/ts-ddraw\n\n; use the following settings to enable the experimental stretching support\n; works only fullscreen right now + menus are not centered\n\n[ddraw]\n; stretch to custom resolution, 0 = defaults to the size game requests\nStretchToWidth=0\nStretchToHeight=0\n\n; override StretchToWidth/StretchToHeight and always stretch to fullscreen\nStretchToFullscreen=No\n\n; use windowboxing to make a best fit\nWindowboxing=No\n\n; maintain aspect ratio - this setting is ignored when Windowboxing is enabled\nMaintainAspectRatio=No\n\n; 0 = never draw FPS, 1 = always draw FPS, 2 = draw FPS only when dropping frames\nDrawFPS=0\n\n; Enable vertical sync\nVSync=No\n\n; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI\nRenderer=gdi\n"
  },
  {
    "path": "DXMainClient/Resources/DTA/ts-ddraw.ini",
    "content": "; ts-ddraw - https://github.com/CnCNet/ts-ddraw\n\n; use the following settings to enable the experimental stretching support\n; works only fullscreen right now + menus are not centered\n\n[ddraw]\n; stretch to custom resolution, 0 = defaults to the size game requests\nStretchToWidth=0\nStretchToHeight=0\n\n; override StretchToWidth/StretchToHeight and always stretch to fullscreen\nStretchToFullscreen=No\n\n; use windowboxing to make a best fit\nWindowboxing=No\n\n; maintain aspect ratio - this setting is ignored when Windowboxing is enabled\nMaintainAspectRatio=No\n\n; 0 = never draw FPS, 1 = always draw FPS, 2 = draw FPS only when dropping frames\nDrawFPS=0\n\n; Enable vertical sync\nVSync=No\n\n; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI\nRenderer=auto\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Base/Instructions.txt",
    "content": "Any .ini file in the \"Base\" folder will be processed and copied to the \"INI\" folder every time a map is loaded.\nThis 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.\nThis 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:\n\n[CIV1]\nName=Civilian\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\nFraidycat=yes\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=67-N100,67-N102\nVoiceMove=67-N104,67-N106,67-N108\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[CIV2]\nBaseSection=CIV1\nCrushSound=SQUISHY2\nVoiceSelect=68-N100,68-N102,68-N104\nVoiceMove=68-N106,68-N108,68-N110\n\n[CIV3]\nBaseSection=CIV1\nVoiceSelect=69-N100,69-N102,69-N104\nVoiceMove=69-N106,69-N108,69-N110\n\n[CIV4]\nBaseSection=CIV1\nImage=CIV1\nFraidycat=no\n\n[CIV5]\nBaseSection=CIV2\nImage=CIV2\nFraidycat=no\n\n[CIV6]\nBaseSection=CIV3\nImage=CIV3\nFraidycat=no\n\n[CTECH]\nBaseSection=CIV6\nName=Technician\nPrimary=Pistola\nAmmo=10\nReload=80\nVoiceSelect=70-N000,70-N002,70-N004\nVoiceMove=70-N006,70-N008,70-N010\nVoiceAttack=70-N014,70-N016,70-N018"
  },
  {
    "path": "DXMainClient/Resources/INI/Battle.ini",
    "content": ";============================================================================\n; BATTLE.INI (English)\n;\n; This control file specifies the battle campaigns that\n; are in the game. Additional files that begin with \"BATTLE\"\n; will augment this list.\n;\n; $Author: Westwood Studios / Rampastring / E1 Elite$\n; $Archive: $\n; $Modtime: 31. 8. 2017$\n; $Revision:$\n;============================================================================\n\n; ******* Battle List *******\n; Lists the various battles in this control file. Each\n; battle is given a unique (internal only) identifier name.\n[Battles]\n0=TSCMPGNS\n1=GDI1\n2=NOD1\n3=EMPTY\n4=FSCMPGNS\n5=GDIFS\n6=NODFS\n7=EMPTY\n8=TUTORIALS\n9=TSDEMO1\n10=TSDEMO2\n;11=EMPTY\n;12=EXTRAS\n;13=GDI1DSHP\n\n; ******* Individual Campaign Data *******\n; Each battle campaign lists its information\n; in a section that cooresponds to its \n; identifier battle name (see above).\n\n; CD = the CD that must be present to play the campaign [-1 means any CD]\n; Scenario = the scenario name for the first mission\n; FinalMovie = finale movie to play at end of campaign (def=none)\n; Description = text description of campaign for player choice list\n; LongDescription = description of campaign for player choice list\n; Side = the ID of the player's side (0 = GDI, 1 = Nod)\n; SideName = determines which texture (side icon) to use for the list entry\n\n[TSCMPGNS]\nDescription=- Tiberian Sun Campaigns -\n\n[GDI1]\nCD=0\nScenario=Maps\\Missions\\GDI1A.MAP\nIntroMovie=INTRO\nFinalMovie=\nDescription=GDI - Evolutionary Response\nLongDescription=The official GDI Campaign of C&C: Tiberian Sun.\nSide=0\nSideName=GDI\n\n[NOD1]\nCD=1\nScenario=Maps\\Missions\\NOD1A.MAP\nIntroMovie=INTRON\nFinalMovie=\nDescription=Brotherhood of Nod - Deus ex Kane\nLongDescription=The official Nod Campaign of C&C: Tiberian Sun.\nSide=1\nSideName=Nod\n\n[FSCMPGNS]\nDescription=- Firestorm Campaigns -\n\n[GDIFS]\nDescription=GDI - Desperate Measures\nScenario=Maps\\Missions\\FSGDI01.MAP\nIntroMovie=FSGDIINT\nCD=2\nRequiredAddon=1\nSide=0\nLongDescription=The official GDI Campaign of C&C: TS - Firestorm.\nSideName=GDI\n\n[NODFS]\nDescription=Brotherhood of Nod - From the Ashes\nScenario=Maps\\Missions\\FSNOD01.MAP\nIntroMovie=FSNODM01\nCD=2\nRequiredAddon=1\nSide=1\nLongDescription=The official Nod Campaign of C&C: TS - Firestorm.\nSideName=Nod\n\n[TUTORIALS]\nDescription=- Tutorial Missions -\n\n[TSDEMO1]\nCD=-1\nScenario=Maps\\Missions\\TSDEMO1.MAP\nFinalMovie=\nDescription=Tutorial 1\nLongDescription=TS Tutorial #1 : Initiation\nSide=0\nSideName=GDI\n\n[TSDEMO2]\nCD=-1\nScenario=Maps\\Missions\\TSDEMO2.MAP\nFinalMovie=\nDescription=Tutorial 2\nLongDescription=TS Tutorial #2 : Clean Sweep\nSide=0\nSideName=GDI\n\n; Adds an empty line to the list\n[EMPTY]\nDescription=\n\n[EXTRAS]\nDescription=- Extras -\n\n[GDI1DSHP]\nCD=0\nScenario=Maps\\Missions\\GDI1ADSHP.MAP\nFinalMovie=\nDescription=Unfinished dropship loadout feature\nLongDescription=GDI 1A - Reinforce Phoenix Base\nSide=0\nSideName=GDI\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Default.ini",
    "content": "[AI]\nParanoid=no\n"
  },
  {
    "path": "DXMainClient/Resources/INI/FSR.ini",
    "content": "[Houses]\n00=GDI\n01=Nod\n02=Neutral\n03=Special\n04=Spawn1\n05=Spawn2\n06=Spawn3\n07=Spawn4\n08=Spawn5\n09=Spawn6\n10=Spawn7\n11=Spawn8\n\n[Spawn1]\nColor=Yellow\n\n[Spawn2]\nColor=HyundaiPurple\n\n[Spawn3]\nColor=Orange\n\n[Spawn4]\nColor=DarkOrange\n\n[Spawn5]\nColor=Magenta\n\n[Spawn6]\nColor=Blue\n\n[Spawn7]\nColor=LightCyan\n\n[Spawn8]\nColor=NeonGreen\n\n[BTIB01]\nImage=BTIB\n\n[BTIB02]\nImage=BTIB\n\n[BTIB03]\nImage=BTIB\n\n[BTIB04]\nImage=BTIB\n\n[BTIB05]\nImage=BTIB\n\n[BTIB06]\nImage=BTIB\n\n[BTIB07]\nImage=BTIB\n\n[BTIB08]\nImage=BTIB\n\n[BTIB09]\nImage=BTIB\n\n[BTIB10]\nImage=BTIB\n\n[BTIB11]\nImage=BTIB\n\n[BTIB12]\nImage=BTIB\n\n[LOBRDG01]\nName=Low Bridge 01\n\n[LOBRDG02]\nName=Low Bridge 02\n\n[LOBRDG03]\nName=Low Bridge 03\n\n[LOBRDG04]\nName=Low Bridge 04\n\n[LOBRDG05]\nName=Low Bridge 05 (Damaged)\n\n[LOBRDG06]\nName=Low Bridge 06 (Damaged)\n\n[LOBRDG07]\nName=Low Bridge 07 (Damaged)\n\n[LOBRDG08]\nName=Low Bridge 08 (Damaged)\n\n[LOBRDG09]\nName=Low Bridge 09 (Damaged)\n\n[LOBRDG10]\nName=Low Bridge 10\n\n[LOBRDG11]\nName=Low Bridge 11\n\n[LOBRDG12]\nName=Low Bridge 12\n\n[LOBRDG13]\nName=Low Bridge 13\n\n[LOBRDG14]\nName=Low Bridge 14 (Damaged)\n\n[LOBRDG15]\nName=Low Bridge 15 (Damaged)\n\n[LOBRDG16]\nName=Low Bridge 16 (Damaged)\n\n[LOBRDG17]\nName=Low Bridge 17 (Damaged)\n\n[LOBRDG18]\nName=Low Bridge 18 (Damaged)\n\n[LOBRDG19]\nName=Low Bridge 19 (End)\n\n[LOBRDG20]\nName=Low Bridge 20 (Damaged End)\n\n[LOBRDG21]\nName=Low Bridge 21 (End)\n\n[LOBRDG22]\nName=Low Bridge 22 (Damaged End)\n\n[LOBRDG23]\nName=Low Bridge 23 (End)\n\n[LOBRDG24]\nName=Low Bridge 24 (Damaged End)\n\n[LOBRDG25]\nName=Low Bridge 25 (End)\n\n[LOBRDG26]\nName=Low Bridge 26 (Damaged End)\n\n[LOBRDG27]\nName=Low Bridge 27 (Destroyed)\n\n[LOBRDG28]\nName=Low Bridge 28 (Destroyed)\n\n[LOBRDGE1]\nName=Low Bridge  End 1\n\n[LOBRDGE2]\nName=Low Bridge  End 2\n\n[LOBRDGE3]\nName=Low Bridge  End 3\n\n[LOBRDGE4]\nName=Low Bridge  End 4\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Auto Deploy MCV.ini",
    "content": "[SUGMCV]\nROT=100\n\n[SUNMCV]\nROT=100\n\n[SUAMCV]\nROT=100\n\n[SUSMCV]\nROT=100\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Disable Super Weapons.ini",
    "content": "[GAPLUG]\nTechLevel=-1\nAIBuildThis=false\n\n[NAMISL]\nTechLevel=-1\nAIBuildThis=false\n\n[NATMPL]\nSuperWeapon=none\n\n[NAWAST]\nTechLevel=-1\nAIBuildThis=false"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Disable Unit Queueing.ini",
    "content": "[General]\nMaximumQueuedObjects=0\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Disable Visceroids.ini",
    "content": "[VISC_LRG]\nStrength=1\nPrimary=RangedSuicide\nGuardRange=512\nTiberiumHeal=no\nExplosion=none\n\n[VISC_SML]\nStrength=1\nPrimary=RangedSuicide\nGuardRange=512\nTiberiumHeal=no\nExplosion=none\n\n[RangedSuicide]\nDamage=0\nROF=10\nRange=512\nProjectile=Invisible\nSpeed=100\nWarhead=Super\nReport=DUMMY\nAnim=ELECTRO"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Extreme AI.ini",
    "content": "[General]\nTeamDelays=525,1175,1250\nMultiplayerAICM=1100,380,200\nMinimumAIDefensiveTeams=1,1,0\nMaximumAIDefensiveTeams=2,2,0\nTotalAITeamCap=16,14,12\n\n[AI]\nBuildBarracks=GFACT_AI,NFACT_AI,AFACT_AI,SFACT_AI\n\n[AIHARV]\nStorage=42\n\n[BuildingTypes]\n1=GFACT_AI\n2=NFACT_AI\n3=AFACT_AI\n4=SFACT_AI\n\n[GFACT]\nAIBuildThis=yes\n\n[NFACT]\nAIBuildThis=yes\n\n[AFACT]\nAIBuildThis=yes\n\n[SFACT]\nAIBuildThis=yes\n\n[GFACT_AI]\nImage=GFACT\nNominal=yes\nName=GDI Construction Yard\nConstructionYard=yes\nStrength=1000\nArmor=wood\nTechLevel=-1\nAdjacent=1\nFactory=BuildingType\nUndeploysInto=GMCV\nSight=5\nOwner=GDI\nCost=1250\nPoints=50\nPower=10\nCapturable=true\nCrewed=yes\nExplosion=FBALL1\nScrapExplosion=FBALL1_SCRAP\nMaxDebris=0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=700, 700, 500\nAIBuildThis=yes\nTogglePower=no\nDeploySound=CONSTRU2\n\n[NFACT_AI]\nImage=NFACT\nNominal=yes\nName=Nod Construction Yard\nConstructionYard=yes\nStrength=1000\nArmor=wood\nTechLevel=-1\nAdjacent=1\nFactory=BuildingType\nUndeploysInto=NMCV\nSight=5\nOwner=Nod\nCost=1250\nPoints=50\nPower=10\nCapturable=true\nCrewed=yes\nExplosion=FBALL1\nScrapExplosion=FBALL1_SCRAP\nMaxDebris=0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=700, 700, 500\nAIBuildThis=yes\nTogglePower=no\nDeploySound=CONSTRU2\n\n[AFACT_AI]\nNominal=yes\nImage=RAFACT\nName=Allied Construction Yard\nConstructionYard=yes\nStrength=1000\nArmor=heavy\nTechLevel=-1\nAdjacent=1\nFactory=BuildingType\nUndeploysInto=AMCV\nSight=5\nOwner=Allies\nCost=1250\nPoints=25\nPower=0\nCapturable=true\nCrewed=yes\nExplosion=FBALL1\nScrapExplosion=FBALL1_SCRAP\nMaxDebris=0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=700, 700, 500\nAIBuildThis=yes\nTogglePower=no\nDeploySound=CONSTRU2\n\n[SFACT_AI]\nNominal=yes\nImage=RAFACT\nName=Soviet Construction Yard\nConstructionYard=yes\nStrength=1000\nArmor=heavy\nTechLevel=-1\nAdjacent=1\nFactory=BuildingType\nUndeploysInto=SMCV\nSight=5\nOwner=Soviet\nCost=1250\nPoints=25\nPower=0\nCapturable=true\nCrewed=yes\nExplosion=FBALL1\nScrapExplosion=FBALL1_SCRAP\nMaxDebris=0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=700, 700, 500\nAIBuildThis=yes\nTogglePower=no\nDeploySound=CONSTRU2\n\n[RAPOWR]\nAIBuildThis=yes\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Harder AI.ini",
    "content": "[General]\nMultiplayerAICM=250,200,100\nHealScanRadius=10\nFillEarliestTeamProbability=100,80,60\nMinimumAIDefensiveTeams=0,0,0\nMaximumAIDefensiveTeams=1,0,0\nTotalAITeamCap=14,12,10\nTeamDelays=500,1800,2200\nAIHateDelays=400,1500,3500\n\n[Easy]\nGroundspeed=1.0\nAirspeed=1.0\nBuildTime=.8\nArmor=1.2\nROF=.8\nCost=0.5\nRepairDelay=.02\nBuildDelay=.03\nDestroyWalls=yes\nContentScan=yes\n\n[Difficult]\nGroundspeed=1.0\nAirspeed=1.0\nBuildTime=2.0\nArmor=.8\nROF=1.2\nCost=0.5\nRepairDelay=.05\nBuildDelay=.1\nBuildSlowdown=yes\nDestroyWalls=no\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Immune Harvesters.ini",
    "content": "[HARV]\nImmune=yes\nCrusher=no\nLegalTarget=no\nInsignificant=yes\nSight=1\nMovementZone=Normal\n\n[HORV]\nImmune=yes\nCrusher=no\nLegalTarget=no\nInsignificant=yes\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Infinite Tiberium.ini",
    "content": "[General]\nHarvesterLoadRate=20.0\nHarvesterDumpRate=0.16\n\n[HARV]\nStorage=3\n\n[TIBTRE01]\nAnimationRate=1\nAnimationProbability=1\n\n[TIBTRE02]\nAnimationRate=1\nAnimationProbability=1\n\n[TIBTRE03]\nAnimationRate=1\nAnimationProbability=1\n\n[Tiberiums]\n0=Riparius\n1=Cruentus\n2=Vinifera\n3=Aboreus\n\n[Riparius]\nName=Tiberium Riparius\nImage=1\nPower=4\nValue=234\nGrowth=2200\nGrowthPercentage=.09\nSpread=2200\nSpreadPercentage=.09\nColor=NeonGreen\n\n[Cruentus]\nName=Tiberium Cruentus\nImage=2\nValue=700\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nPower=10\nColor=NeonBlue\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n[Vinifera]\nName=Tiberium Vinifera\nImage=3\nValue=467\nGrowth=10000\nGrowthPercentage=.05\nSpread=10000\nSpreadPercentage=.05\nPower=100 ; 10\nColor=NeonBlue\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n[Aboreus]\nName=Tiberium Aboreus\nImage=4\nValue=300\nGrowth=10000\nGrowthPercentage=.05\nSpread=10000\nSpreadPercentage=.05\nPower=10\nColor=NeonBlue\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Ingame Allying.ini",
    "content": "[Tutorial]\n925=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.\n\n[Tags]\nIngameAllyingTag=0,Ingame Allying 1,IngameAllying\n\n[Triggers]\nIngameAllying=Neutral,<none>,Ingame Allying,0,1,1,1,0\n\n[Events]\nIngameAllying=1,8,0,0\n\n[Actions]\nIngameAllying=1,11,0,925,0,0,0,0,A\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Instant Harvester Unload.ini",
    "content": "[General]\nHarvesterDumpRate=0\n\n[PROC]\nImage=PROCI\n\n[TDPROC]\nImage=TDPROCI\n\n[RAPROC]\nImage=RAPROCI\n\n[PROC_AI]\nImage=PROCI\n\n[TDPROC_AI]\nImage=TDPROCI\n\n[RAPROC_AI]\nImage=RAPROCI\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Naval.ini",
    "content": "[GSYRD]\nTechLevel=5\n\n[NSYRD]\nTechLevel=5\n\n[ASYRD]\nTechLevel=5\n\n[RASPEN]\nTechLevel=5\n\n[GSYRD_AI]\nTechLevel=5\n\n[NSYRD_AI]\nTechLevel=5\n\n[ASYRD_AI]\nTechLevel=5\n\n[RASPEN_AI]\nTechLevel=5\n\n[WEAP_AI]\nWeaponsFactory=no\n\n[AFLD_AI]\nWeaponsFactory=no\n\n[AWEAP_AI]\nWeaponsFactory=no\n\n[SWEAP_AI]\nWeaponsFactory=no"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/No Baddy Crates.ini",
    "content": "[Powerups]\nArmor=33,ARMOR,0.5\nCloak=20,CLOAK\nDarkness=0,SHROUDX\nExplosion=0,RAPID,0\nFirepower=28,FIREPOWR,2.0\nHealBase=23,HEALALL\nICBM=10,CHEMISLE\nMoney=35,MONEY,2000\nNapalm=0,<none>,0\nReveal=0,REVEAL\nSpeed=30,SPEED,1.7\nSquad=0,<none>\nUnit=70,<none>\nInvulnerability=15,AREAHEAL,1.0\nVeteran=30,VETERAN,1\nIonStorm=0,<none>\nGas=0,<none>,0\nTiberium=25,<none>\nPod=0,<none>"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/No Crew.ini",
    "content": "[General]\nCrewEscape=0%\n\n[GFACT]\nCrewed=no\n\n[NFACT]\nCrewed=no\n\n[NUKE]\nCrewed=no\n\n[NUK2]\nCrewed=no\n\n[PROC]\nCrewed=no\n\n[PYLE]\nCrewed=no\n\n[HAND]\nCrewed=no\n\n[TWR]\nCrewed=no\n\n[GUN]\nCrewed=no\n\n[RAGUN]\nCrewed=no\n\n[SAM]\nCrewed=no\n\n[ATWR]\nCrewed=no\n\n[OBLI]\nCrewed=no\n\n[DTERM]\nCrewed=no\n\n[HQ]\nCrewed=no\n\n[NHQ]\nCrewed=no\n\n[GHPAD]\nCrewed=no\n\n[NHPAD]\nCrewed=no\n\n[AHPAD]\nCrewed=no\n\n[ASTRP]\nCrewed=no\n\n[RAASTRP]\nCrewed=no\n\n[WEAP]\nCrewed=no\n\n[AFLD]\nCrewed=no\n\n[FIX]\nCrewed=no\n\n[EYE]\nCrewed=no\n\n[COMM]\nCrewed=no\n\n[SGEN]\nCrewed=no\n\n[TMPL]\nCrewed=no\n\n[MISS]\nCrewed=no\n\n[AFACT]\nCrewed=no\n\n[SFACT]\nCrewed=no\n\n[RAPOWR]\nCrewed=no\n\n[RAAPWR]\nCrewed=no\n\n[RATENT]\nCrewed=no\n\n[RABARR]\nCrewed=no\n\n[RAKENN]\nCrewed=no\n\n[RAPROC]\nCrewed=no\n\n[RATSLA]\nCrewed=no\n\n[RADOME]\nCrewed=no\n\n[AWEAP]\nCrewed=no\n\n[SWEAP]\nCrewed=no\n\n[RAFCOM]\nCrewed=no\n\n[RAFIX]\nCrewed=no\n\n[RAATEK]\nCrewed=no\n\n[RASTEK]\nCrewed=no\n\n[RAIRON]\nCrewed=no\n\n[NAMSLO]\nCrewed=no\n\n[AMSLO]\nCrewed=no\n\n[RAPDOX]\nCrewed=no\n\n[NUKE_AI]\nCrewed=no\n\n[NUK2_AI]\nCrewed=no\n\n[NUK2A_AI]\nCrewed=no\n\n[NUK2B_AI]\nCrewed=no\n\n[NUK2C_AI]\nCrewed=no\n\n[NUK2D_AI]\nCrewed=no\n\n[NUK2E_AI]\nCrewed=no\n\n[NUK2G_AI]\nCrewed=no\n\n[RAPOWR_AI]\nCrewed=no\n\n[RAAPWR_AI]\nCrewed=no\n\n[PROC_AI]\nCrewed=no\n\n[PYLE_AI]\nCrewed=no\n\n[HAND_AI]\nCrewed=no\n\n[RATENT_AI]\nCrewed=no\n\n[RABARR_AI]\nCrewed=no\n\n[TWR_AI]\nCrewed=no\n\n[OBLI_AI]\nCrewed=no\n\n[RAGUN_AI]\nCrewed=no\n\n[RATSLA_AI]\nCrewed=no\n\n[HPAD_AI]\nCrewed=no\n\n[AHPAD_AI]\nCrewed=no\n\n[WEAP_AI]\nCrewed=no\n\n[AFLD_AI]\nCrewed=no\n\n[AWEAP_AI]\nCrewed=no\n\n[SWEAP_AI]\nCrewed=no\n\n[EYE_AI]\nCrewed=no\n\n[RAATEK_AI]\nCrewed=no\n\n[RASTEK_AI]\nCrewed=no\n\n[RAMSLO_AI]\nCrewed=no\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/No Silos.ini",
    "content": "[Units]\nK=Spawn1,STORAGEU,256,0,2,64,Unload,None,0,-1,0,-1,1,0\nL=Spawn2,STORAGEU,256,1,2,64,Unload,None,0,-1,0,-1,1,0\nM=Spawn3,STORAGEU,256,2,2,64,Unload,None,0,-1,0,-1,1,0\nN=Spawn4,STORAGEU,256,3,2,64,Unload,None,0,-1,0,-1,1,0\nO=Spawn5,STORAGEU,256,4,2,64,Unload,None,0,-1,0,-1,1,0\nP=Spawn6,STORAGEU,256,5,2,64,Unload,None,0,-1,0,-1,1,0\nQ=Spawn7,STORAGEU,256,6,2,64,Unload,None,0,-1,0,-1,1,0\nR=Spawn8,STORAGEU,256,7,2,64,Unload,None,0,-1,0,-1,1,0\n\n[PROC]\nStorage=0\nPipScale=none\n\n[SILO]\nTechLevel=-1\n\n[VehicleTypes]\n1=STORAGEU\n\n[STORAGEU]\nNominal=yes\nROT=16\nStrength=1\nSelectable=false\nInsignificant=yes\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nDeploysInto=STORAGE\nSecondary=RangedSuicide\nDeployToFire=yes\nGuardRange=512\n\n[RangedSuicide]\nDamage=0\nROF=10\nRange=512\nProjectile=Invisible\nSpeed=100\nWarhead=Super\n\n[BuildingTypes]\n1=STORAGE\n\n[STORAGE]\nStorage=99999999\nImage=GALITE\nStrength=600\nArmor=wood\nTogglePower=no\nSelectable=no\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nBridgeRepairHut=yes\nPlaceAnywhere=yes\nInvisibleInGame=yes\nBaseNormal=no\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Replace Tiberium With Ore.ini",
    "content": "[Tiberiums]\n0=Riparius\n1=Cruentus\n2=Vinifera\n3=Aboreus\n\n[Riparius]\nName=Ore\nColor=DarkGold\nImage=1\nPower=0\nValue=25\nGrowth=1000\nSpread=1000\nGrowthPercentage=10\nSpreadPercentage=2\n\n[Vinifera]\nName=Gems\nColor=Red\nImage=3\nPower=0\nValue=40\nGrowth=1000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\n\n[Cruentus]\nName=Gems\nColor=Red\nImage=2\nPower=0\nValue=40\nGrowth=1000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\n\n[TIBTREE]\nImage=OREMINE2\nName=Ore Mine\n\n[TIBTREE2]\nImage=OREMINE2\nName=Ore Mine\n\n[TIBTREE3]\nImage=OREMINE2\nName=Ore Mine\n\n[VINTREE]\nImage=OREMINE3\nName=Gem Mine\n\n[VINTREE2]\nImage=OREMINE3\nName=Gem Mine\n\n[VINTREE3]\nImage=OREMINE3\nName=Gem Mine\n\n[OREMINE3]\nImage=OREMINE2\n\n[RTIB01]\nImage=ORE01\nChainReaction=no\n\n[RTIB02]\nImage=ORE02\nChainReaction=no\n\n[RTIB03]\nImage=ORE03\nChainReaction=no\n\n[RTIB04]\nImage=ORE04\nChainReaction=no\n\n[RTIB05]\nImage=ORE01\nChainReaction=no\n\n[RTIB06]\nImage=ORE02\nChainReaction=no\n\n[RTIB07]\nImage=ORE03\nChainReaction=no\n\n[RTIB08]\nImage=ORE04\nChainReaction=no\n\n[RTIB09]\nImage=ORE01\nChainReaction=no\n\n[RTIB10]\nImage=ORE02\nChainReaction=no\n\n[RTIB11]\nImage=ORE03\nChainReaction=no\n\n[RTIB12]\nImage=ORE04\nChainReaction=no\n\n[QTIB01]\nImage=GEMRA01\nChainReaction=no\n\n[QTIB02]\nImage=GEMRA02\nChainReaction=no\n\n[QTIB03]\nImage=GEMRA03\nChainReaction=no\n\n[QTIB04]\nImage=GEMRA04\nChainReaction=no\n\n[QTIB05]\nImage=GEMRA01\nChainReaction=no\n\n[QTIB06]\nImage=GEMRA02\nChainReaction=no\n\n[QTIB07]\nImage=GEMRA03\nChainReaction=no\n\n[QTIB08]\nImage=GEMRA04\nChainReaction=no\n\n[QTIB09]\nImage=GEMRA01\nChainReaction=no\n\n[QTIB10]\nImage=GEMRA02\nChainReaction=no\n\n[QTIB11]\nImage=GEMRA03\nChainReaction=no\n\n[QTIB12]\nImage=GEMRA04\nChainReaction=no\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Reveal Shroud.ini",
    "content": "[Events]\nRevealOption=1,8,0,0\n\n[Actions]\nRevealOption=1,16,0,0,0,0,0,0,A\n\n[Tags]\nRevealOptionTag=0,Reveal 1,RevealOption\n\n[Triggers]\nRevealOption=Neutral,<none>,Reveal,0,1,1,1,0"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Shroud Regrows.ini",
    "content": "[AudioVisual]\nShroudGrow=yes\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Starting Units.ini",
    "content": "[StartingUnitsScript]\n0=21,11\nName=Scatter\n\n[1TNK-SUTaskForce]\n0=1,1TNK\nName=Starting 1TNK\nGroup=-1\n\n[2TNK-SUTaskForce]\n0=1,2TNK\nName=Starting 2TNK\nGroup=-1\n\n[3TNK-SUTaskForce]\n0=1,3TNK\nName=Starting 3TNK\nGroup=-1\n\n[Actions]\nStartingUnits1=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\nStartingUnits2=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\nStartingUnits3=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\nStartingUnits4=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\nStartingUnits5=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\nStartingUnits6=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\nStartingUnits7=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\nStartingUnits8=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\n\n[Events]\nStartingUnits1=1,13,0,50\nStartingUnits2=1,13,0,50\nStartingUnits3=1,13,0,50\nStartingUnits4=1,13,0,50\nStartingUnits5=1,13,0,50\nStartingUnits6=1,13,0,50\nStartingUnits7=1,13,0,50\nStartingUnits8=1,13,0,50\n\n[Infantry-SUTaskForce]\n0=1,E2\n1=1,E4\n2=1,E1\nName=Starting Infantry\nGroup=-1\n\n[HVCT-SUTaskForce]\n0=1,HVCT\nName=Starting HVCT\nGroup=-1\n\n[MTNK-SUTaskForce]\n0=1,MTNK\nName=Starting MTNK\nGroup=-1\n\n[ScriptTypes]\nSU=StartingUnitsScript\n\n[Spawn1-1TNK]\nMax=5\nFull=yes\nName=Spawn1 Starting 1TNK\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn1-2TNK]\nMax=5\nFull=yes\nName=Spawn1 Starting 2TNK\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn1-3TNK]\nMax=5\nFull=yes\nName=Spawn1 Starting 3TNK\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn1-Infantry]\nMax=5\nFull=yes\nName=Spawn1 Starting Infantry\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn1-HVCT]\nMax=5\nFull=yes\nName=Spawn1 Starting HVCT\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn1-MTNK]\nMax=5\nFull=yes\nName=Spawn1 Starting MTNK\nGroup=-1\nHouse=Spawn1\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=A\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-1TNK]\nMax=5\nFull=yes\nName=Spawn2 Starting 1TNK\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-2TNK]\nMax=5\nFull=yes\nName=Spawn2 Starting 2TNK\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-3TNK]\nMax=5\nFull=yes\nName=Spawn2 Starting 3TNK\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-Infantry]\nMax=5\nFull=yes\nName=Spawn2 Starting Infantry\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-HVCT]\nMax=5\nFull=yes\nName=Spawn2 Starting HVCT\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn2-MTNK]\nMax=5\nFull=yes\nName=Spawn2 Starting MTNK\nGroup=-1\nHouse=Spawn2\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=B\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-1TNK]\nMax=5\nFull=yes\nName=Spawn3 Starting 1TNK\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-2TNK]\nMax=5\nFull=yes\nName=Spawn3 Starting 2TNK\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-3TNK]\nMax=5\nFull=yes\nName=Spawn3 Starting 3TNK\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-Infantry]\nMax=5\nFull=yes\nName=Spawn3 Starting Infantry\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-HVCT]\nMax=5\nFull=yes\nName=Spawn3 Starting HVCT\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn3-MTNK]\nMax=5\nFull=yes\nName=Spawn3 Starting MTNK\nGroup=-1\nHouse=Spawn3\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=C\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-1TNK]\nMax=5\nFull=yes\nName=Spawn4 Starting 1TNK\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-2TNK]\nMax=5\nFull=yes\nName=Spawn4 Starting 2TNK\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-3TNK]\nMax=5\nFull=yes\nName=Spawn4 Starting 3TNK\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-Infantry]\nMax=5\nFull=yes\nName=Spawn4 Starting Infantry\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-HVCT]\nMax=5\nFull=yes\nName=Spawn4 Starting HVCT\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn4-MTNK]\nMax=5\nFull=yes\nName=Spawn4 Starting MTNK\nGroup=-1\nHouse=Spawn4\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=D\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-1TNK]\nMax=5\nFull=yes\nName=Spawn5 Starting 1TNK\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-2TNK]\nMax=5\nFull=yes\nName=Spawn5 Starting 2TNK\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-3TNK]\nMax=5\nFull=yes\nName=Spawn5 Starting 3TNK\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-Infantry]\nMax=5\nFull=yes\nName=Spawn5 Starting Infantry\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-HVCT]\nMax=5\nFull=yes\nName=Spawn5 Starting HVCT\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn5-MTNK]\nMax=5\nFull=yes\nName=Spawn5 Starting MTNK\nGroup=-1\nHouse=Spawn5\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=E\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-1TNK]\nMax=5\nFull=yes\nName=Spawn6 Starting 1TNK\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-2TNK]\nMax=5\nFull=yes\nName=Spawn6 Starting 2TNK\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-3TNK]\nMax=5\nFull=yes\nName=Spawn6 Starting 3TNK\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-Infantry]\nMax=5\nFull=yes\nName=Spawn6 Starting Infantry\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-HVCT]\nMax=5\nFull=yes\nName=Spawn6 Starting HVCT\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn6-MTNK]\nMax=5\nFull=yes\nName=Spawn6 Starting MTNK\nGroup=-1\nHouse=Spawn6\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=F\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-1TNK]\nMax=5\nFull=yes\nName=Spawn7 Starting 1TNK\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-2TNK]\nMax=5\nFull=yes\nName=Spawn7 Starting 2TNK\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-3TNK]\nMax=5\nFull=yes\nName=Spawn7 Starting 3TNK\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-Infantry]\nMax=5\nFull=yes\nName=Spawn7 Starting Infantry\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-HVCT]\nMax=5\nFull=yes\nName=Spawn7 Starting HVCT\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn7-MTNK]\nMax=5\nFull=yes\nName=Spawn7 Starting MTNK\nGroup=-1\nHouse=Spawn7\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=G\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-1TNK]\nMax=5\nFull=yes\nName=Spawn8 Starting 1TNK\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=1TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-2TNK]\nMax=5\nFull=yes\nName=Spawn8 Starting 2TNK\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=2TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-3TNK]\nMax=5\nFull=yes\nName=Spawn8 Starting 3TNK\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=3TNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-Infantry]\nMax=5\nFull=yes\nName=Spawn8 Starting Infantry\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=Infantry-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-HVCT]\nMax=5\nFull=yes\nName=Spawn8 Starting HVCT\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=HVCT-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Spawn8-MTNK]\nMax=5\nFull=yes\nName=Spawn8 Starting MTNK\nGroup=-1\nHouse=Spawn8\nScript=StartingUnitsScript\nWhiner=no\nDroppod=no\nSuicide=no\nLoadable=no\nPrebuild=no\nPriority=5\nWaypoint=H\nAnnoyance=no\nIonImmune=no\nRecruiter=no\nReinforce=no\nTaskForce=MTNK-SUTaskForce\nTechLevel=0\nAggressive=no\nAutocreate=yes\nGuardSlower=no\nOnTransOnly=no\nAvoidThreats=no\nLooseRecruit=no\nVeteranLevel=1\nIsBaseDefense=no\nOnlyTargetHouseEnemy=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=no\n\n[Tags]\nStartingUnits1Tag=0,Spawn1 Starting Units 1,StartingUnits1\nStartingUnits2Tag=0,Spawn2 Starting Units 1,StartingUnits2\nStartingUnits3Tag=0,Spawn3 Starting Units 1,StartingUnits3\nStartingUnits4Tag=0,Spawn4 Starting Units 1,StartingUnits4\nStartingUnits5Tag=0,Spawn5 Starting Units 1,StartingUnits5\nStartingUnits6Tag=0,Spawn6 Starting Units 1,StartingUnits6\nStartingUnits7Tag=0,Spawn7 Starting Units 1,StartingUnits7\nStartingUnits8Tag=0,Spawn8 Starting Units 1,StartingUnits8\n\n[TaskForces]\nSU0=MTNK-SUTaskForce\nSU1=HVCT-SUTaskForce\nSU2=1TNK-SUTaskForce\nSU3=2TNK-SUTaskForce\nSU4=3TNK-SUTaskForce\nSU5=Infantry-SUTaskForce\n\n[TeamTypes]\nSU00=Spawn1-MTNK\nSU01=Spawn2-MTNK\nSU02=Spawn3-MTNK\nSU03=Spawn4-MTNK\nSU04=Spawn5-MTNK\nSU05=Spawn6-MTNK\nSU06=Spawn7-MTNK\nSU07=Spawn8-MTNK\nSU08=Spawn1-HVCT\nSU09=Spawn1-1TNK\nSU10=Spawn1-2TNK\nSU11=Spawn1-3TNK\nSU12=Spawn1-Infantry\nSU13=Spawn4-HVCT\nSU14=Spawn2-HVCT\nSU15=Spawn2-1TNK\nSU16=Spawn2-2TNK\nSU17=Spawn2-3TNK\nSU18=Spawn2-Infantry\nSU19=Spawn4-1TNK\nSU20=Spawn3-HVCT\nSU21=Spawn3-1TNK\nSU22=Spawn3-2TNK\nSU23=Spawn3-3TNK\nSU24=Spawn3-Infantry\nSU25=Spawn4-2TNK\nSU26=Spawn4-3TNK\nSU27=Spawn4-Infantry\nSU28=Spawn5-HVCT\nSU29=Spawn5-1TNK\nSU30=Spawn5-2TNK\nSU31=Spawn5-3TNK\nSU32=Spawn5-Infantry\nSU33=Spawn6-HVCT\nSU34=Spawn6-1TNK\nSU35=Spawn6-2TNK\nSU36=Spawn6-3TNK\nSU37=Spawn6-Infantry\nSU38=Spawn7-HVCT\nSU39=Spawn7-1TNK\nSU40=Spawn7-2TNK\nSU41=Spawn7-3TNK\nSU42=Spawn7-Infantry\nSU43=Spawn8-HVCT\nSU44=Spawn8-1TNK\nSU45=Spawn8-2TNK\nSU46=Spawn8-3TNK\nSU47=Spawn8-Infantry\n\n[Triggers]\nStartingUnits1=Neutral,<none>,Spawn1 Starting Units,0,1,1,1,0\nStartingUnits2=Neutral,<none>,Spawn2 Starting Units,0,1,1,1,0\nStartingUnits3=Neutral,<none>,Spawn3 Starting Units,0,1,1,1,0\nStartingUnits4=Neutral,<none>,Spawn4 Starting Units,0,1,1,1,0\nStartingUnits5=Neutral,<none>,Spawn5 Starting Units,0,1,1,1,0\nStartingUnits6=Neutral,<none>,Spawn6 Starting Units,0,1,1,1,0\nStartingUnits7=Neutral,<none>,Spawn7 Starting Units,0,1,1,1,0\nStartingUnits8=Neutral,<none>,Spawn8 Starting Units,0,1,1,1,0\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Storms.ini",
    "content": "[Actions]\nStormsOption=1,44,0,300,0,0,0,0,A\n\n[Events]\nStormsOption=1,51,0,1000\n\n[Tags]\nStormsOptionTag=2,Ion Storm 1,StormsOption\n\n[Triggers]\nStormsOption=Neutral,<none>,Ion Storm,0,1,1,1,0\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Turbo Vehicles.ini",
    "content": "[Clear]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Rough]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Road]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Water]\nHover=69%\nFloat=69%\nAmphibious=69%\n\n[Tiberium]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Weeds]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Beach]\nHover=69%\nAmphibious=69%\n\n[Railroad]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[Ice]\nHover=69%\nAmphibious=69%\n\n[Tunnel]\nTrack=69%\nWheel=69%\nHover=69%\nAmphibious=69%\n\n[LTNK]\nSpeed=8\n\n[1TNK]\nSpeed=9\n\n[MTNK]\nSpeed=8\n\n[2TNK]\nSpeed=8\n\n[FTNK]\nSpeed=8\n\n[3TNK]\nSpeed=7\n\n[SOVAPC]\nSpeed=8\n\n[MFLAK]\nSpeed=8\n\n[DTRK]\nSpeed=8\n\n[TERMITE]\nSpeed=8\n\n[MWAVE]\nSpeed=7\n\n[TTNK]\nSpeed=8\n\n[ARTY]\nSpeed=6\n\n[RAARTY]\nSpeed=6\n\n[MSAM]\nSpeed=8\n\n[MLRS]\nSpeed=8\n\n[V2RL]\nSpeed=7\n\n[XO]\nSpeed=8\n\n[SSM]\nSpeed=8\n\n[SAPC]\nSpeed=7\n\n[MRV]\nSpeed=8\n\n[MSA]\nSpeed=8\n\n[AIXO]\nSpeed=8\n\n[AIMTNK]\nSpeed=8\n\n[AILTNK]\nSpeed=8\n\n[AIFTNK]\nSpeed=8\n\n[AIMWAVE]\nSpeed=\n\n[AIMLRS]\nSpeed=8\n\n[AIARTY]\nSpeed=6\n\n[AIMSAM]\nSpeed=8\n\n[AI2TNK]\nSpeed=8\n\n[AI3TNK]\nSpeed=7\n\n[AIV2RL]\nSpeed=7\n\n[AITTNK]\nSpeed=8\n\n[AIHVCT]\nSpeed=8\n\n[AIMFLAK]\nSpeed=8\n\n[HVCT]\nSpeed=8\n\n[HVR]\nSpeed=10\n\n[HVC]\nSpeed=8\n\n[HVCSAM]\nSpeed=7\n\n[LTNKCRUS]\nSpeed=8\n\n[MWAVEMSAM]\nSpeed=7\n\n[UTNK]\nSpeed=7\n\n[CUMRV]\nSpeed=8\n\n[CUMSA]\nSpeed=8\n\n[ANT1]\nSpeed=8\n\n[ANT2]\nSpeed=8\n\n[ANT3]\nSpeed=8\n\n[ANT4]\nSpeed=8\n\n[ANT5]\nSpeed=8\n\n;[MHQ]\n;Speed=8\t;6\n;\n;[HARV]\n;Speed=8\t;6\n;\n;[GMCV]\n;Speed=8\t;6\n;\n;[NMCV]\n;Speed=8\t;6\n;\n;[AMCV]\n;Speed=8\t;6\n;\n;[SMCV]\n;Speed=8\t;6\n;\n;[SUGMCV]\n;Speed=8\t;6\n;\n;[SUNMCV]\n;Speed=8\t;6\n;\n;[SUAMCV]\n;Speed=8\t;6\n;\n;[SUSMCV]\n;Speed=8\t;6\n;\n;[FUHARV]\n;Speed=8\t;6\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Turtling AI.ini",
    "content": "[General]\nAIHateDelays=99999999,99999999,99999999\n\n[TaskForces]\nTurtlingAI01=08603140-G\nTurtlingAI02=07EA1E90-G\nTurtlingAI03=08602820-G\nTurtlingAI04=0860DE90-G\nTurtlingAI05=09F7B380-G\nTurtlingAI06=0860314M-G\nTurtlingAI07=09F7B38M-G\nTurtlingAI08=00GAIRE1-G\nTurtlingAI09=00NAIRE1-G\nTurtlingAI10=0860282M-G\nTurtlingAI11=0860DE9M-G\nTurtlingAI12=08601B60-A\nTurtlingAI13=00GAIRE1-A\nTurtlingAI14=0860314M-A\nTurtlingAI15=07EA1E9M-A\nTurtlingAI16=09F7B38M-A\nTurtlingAI17=08603140-A\nTurtlingAI18=07EA1E90-A\nTurtlingAI19=09F7B380-A\nTurtlingAI20=00GAIRE1-S\nTurtlingAI21=0860314M-S\nTurtlingAI22=07EA1E9M-S\nTurtlingAI23=09F7B38M-S\nTurtlingAI24=08603140-S\nTurtlingAI25=09F7B380-S\nTurtlingAI26=07EA1E90-S\n\n[08603140-G]\n0=1,ORCA\n\n[07EA1E90-G]\n0=1,ORCA\n\n[08602820-G]\n0=1,APACHE\n\n[0860DE90-G]\n0=1,APACHE\n\n[09F7B380-G]\n0=1,ORCA\n\n[0860314M-G]\n0=1,ORCA\n\n[09F7B38M-G]\n0=1,ORCA\n\n[00GAIRE1-G]\n0=1,ORCA\n\n[00NAIRE1-G]\n0=1,APACHE\n\n[0860282M-G]\n0=1,APACHE\n\n[0860DE9M-G]\n0=1,APACHE\n\n[08601B60-A]\n0=1,HELI\n\n[00GAIRE1-A]\n0=1,HELI\n\n[0860314M-A]\n0=1,HELI\n\n[07EA1E9M-A]\n0=1,HELI\n\n[09F7B38M-A]\n0=1,HELI\n\n[08603140-A]\n0=1,HELI\n\n[07EA1E90-A]\n0=1,HELI\n\n[09F7B380-A]\n0=1,HELI\n\n[00GAIRE1-S]\n0=1,YAK\n\n[0860314M-S]\n1=1,MIG\n\n[07EA1E9M-S]\n0=1,MIG\n\n[09F7B38M-S]\n0=1,YAK\n\n[08603140-S]\n1=1,MIG\n\n[09F7B380-S]\n0=1,YAK\n\n[07EA1E90-S]\n0=1,MIG\n\n[ScriptTypes]\nTurtlingAI0=085F3E00-G\nTurtlingAI1=085F3980-G\nTurtlingAI2=075A3070-G\nTurtlingAI3=07F7B2A0-G\nTurtlingAI4=07E686F0-G\nTurtlingAI5=07F7C5E0-G\nTurtlingAI6=0786DA60-G\nTurtlingAI7=07F7D0D0-G\nTurtlingAI8=07F7E3B0-G\nTurtlingAI9=0960AAA0-G\nTurtlingAI10=07F76BE0-G\nTurtlingAI11=075AD760-G\nTurtlingAI12=08462780-G\nTurtlingAI13=075ABE00-G\nTurtlingAI14=08463030-G\nTurtlingAI15=088DDE00-G\nTurtlingAI16=07397BE0-G\nTurtlingAI17=07F3DE00-G\nTurtlingAI18=08B50140-G\nTurtlingAI19=0H000000-G\nTurtlingAI20=0TR00000-G\nTurtlingAI21=0TR00001-G\nTurtlingAI22=0HUNT000-G\nTurtlingAI23=MCV_IDLE\nTurtlingAI24=MCV_DEPLOY\nTurtlingAI1019=GNAVALA0-G\nTurtlingAI1020=NNAVALA0-G\nTurtlingAI1021=SNAVALA0-G\nTurtlingAI1023=00NODCY0-G\nTurtlingAI1024=00SOVCY0-G\nTurtlingAI1025=00ALDCY0-G\nTurtlingAI1027=0HHH0000-G\n\n[085F3E00-G]\n0=11,10\n\n[085F3980-G]\n0=11,10\n\n[075A3070-G]\n0=11,10\n\n[07F7B2A0-G]\n0=11,10\n\n[07E686F0-G]\n0=11,10\n\n[07F7C5E0-G]\n0=11,10\n\n[0786DA60-G]\n0=11,10\n\n[07F7D0D0-G]\n0=11,10\n\n[07F7E3B0-G]\n0=11,10\n\n[0960AAA0-G]\n0=11,10\n\n[07F76BE0-G]\n0=11,10\n\n[075AD760-G]\n0=11,10\n\n[08462780-G]\n0=11,10\n\n[075ABE00-G]\n0=11,10\n\n[08463030-G]\n0=11,10\n\n[088DDE00-G]\n0=11,10\n\n[07F3DE00-G]\n0=11,10\n\n[08B50140-G]\n0=9,0\n\n[07397BE0-G]\n0=11,10\n\n[0H000000-G]\n0=11,10\n\n[0TR00000-G]\n0=11,10\n\n[0TR00001-G]\n0=11,10\n\n[0HUNT000-G]\n0=11,10\n\n[GNAVALA0-G]\n0=11,10\n\n[NNAVALA0-G]\n0=11,10\n\n[SNAVALA0-G]\n0=11,10\n\n[00NODCY0-G]\n0=11,10\n\n[00SOVCY0-G]\n0=11,10\n\n[00ALDCY0-G]\n0=11,10\n\n[0HHH0000-G]\n0=11,10\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Uncrushable Infantry.ini",
    "content": "[E1]\nCrushable=no\n\n[E1N]\nCrushable=no\n\n[E1A]\nCrushable=no\n\n[E1S]\nCrushable=no\n\n[E2]\nCrushable=no\n\n[E2S]\nCrushable=no\n\n[E3]\nCrushable=no\n\n[E3N]\nCrushable=no\n\n[E3A]\nCrushable=no\n\n[E3S]\nCrushable=no\n\n[E4]\nCrushable=no\n\n[E4S]\nCrushable=no\n\n[E5]\nCrushable=no\n\n[MEDIC]\nCrushable=no\n\n[ENGINEER]\nCrushable=no\n\n[DOG]\nCrushable=no\n\n[RMBO]\nCrushable=no\n\n[TANYA]\nCrushable=no\n\n[CTECH]\nCrushable=no\n\n[MOEBIUS]\nCrushable=no\n\n[DELPHI]\nCrushable=no\n\n[CHAN]\nCrushable=no\n\n[RAPT]\nCrushable=no\n\n[D1]\nCrushable=no\n\n[D2]\nCrushable=no\n\n[SUE1A]\nCrushable=no\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Game Options/Veteran Balance Patch.ini",
    "content": "[CnCNet]\nAuthor=Humble/Skylegend/Xme/NME/Mola/Carnage/Cambria/Trooper/Black/Etc./Etc.\nWebsite=https://github.com/HumbleTS/Balance-Veteran-Patch/wiki/Balance-Veteran-Summary\nVersion=2.50\n\n[General]\nVeteranROF=.20\nVeteranArmor=.20\nVeteranRatio=3.5\nVeteranSpeed=.20\nVeteranCombat=.20\nVeinholeGrowthRate=1\n\n[Powerups]\nGas=0,<none>,100\nPod=1,<none>\nICBM=1,CHEMISLE\nUnit=0,<none>\nArmor=1,ARMOR,2.0\nCloak=1,CLOAK\nMoney=0,MONEY,2000\nSpeed=1,ARMOR,1.7\nSquad=0,<none>\nNapalm=0,<none>,600\nReveal=0,REVEAL\nVeteran=1,VETERAN,1\nDarkness=0,SHROUDX\nHealBase=0,HEALALL\nIonStorm=0,<none>\nTiberium=0,<none>\nExplosion=0,<none>,500\nFirepower=1,FIREPOWR,2.0\nInvulnerability=0,ARMOR,1.0\n\n[E1]\nTrainable=no\n\n[E2]\nTrainable=no\n\n[E3]\nTrainable=no\n\n[CYBORG]\nTrainable=no\n\n[JUMPJET]\nTrainable=no\n\n[GHOST]\nElite=EliteLtRail\nTrainable=yes\nEliteAbilities=SELF_HEAL,FASTER\n\n[CYC2]\nElite=EliteCyCannon\nTrainable=yes\nEliteAbilities=SELF_HEAL,FASTER\n\n[DOGGIE]\nCost=330\nElite=EliteFiendShard\nEliteAbilities=STRONGER,SELF_HEAL\n\n[SMECH]\nElite=EliteAssaultCannon\nEliteAbilities=FIREPOWER,STRONGER\n\n[BGGY]\nElite=EliteRaiderCannon\nEliteAbilities=FIREPOWER,STRONGER\n\n[BIKE]\nElite=EliteBikeMissile\nEliteAbilities=FIREPOWER,STRONGER\n\n[MMCH]\nElite=Elite120mm\nEliteAbilities=FIREPOWER,STRONGER\n\n[TTNK]\nArmor=heavy\nElite=Elite90mm\nEliteAbilities=FIREPOWER,STRONGER\nTooBigToFitUnderBridge=true\n\n[GATICK]\nElite=Elite90mm\nEliteAbilities=FIREPOWER,STRONGER\n\n[ART2]\nElite=Elite155mm\nEliteAbilities=FIREPOWER,STRONGER\nTooBigToFitUnderBridge=true\n\n[GAARTY]\nElite=Elite155mm\nEliteAbilities=FIREPOWER,STRONGER\n\n[HVR]\nElite=EliteHoverMissile\nEliteAbilities=FIREPOWER,STRONGER\n\n[STNK]\nElite=EliteDragon\nArmour=wood\nTurret=Yes\nStrength=230\nCloakingSpeed=3\nEliteAbilities=STRONGER,SELF_HEAL\n\n[SONIC]\nTrainable=no\n\n[HMEC]\nElite=EliteMechRailgun\nPrimary=MechRailgun\nSecondary=MammothTusk\nTrainable=yes\nEliteAbilities=STRONGER,FASTER\n\n[WEED]\nThreatAvoidanceCoefficient=0.00\n\n[VISC_LRG]\nCost=330\nElite=EliteSlimeAttack\nTrainable=Yes\nEliteAbilities=STRONGER,SELF_HEAL\n\n[ORCA]\nElite=EliteHellfire\nEliteAbilities=FIREPOWER,SELF_HEAL\n\n[APACHE]\nElite=EliteHarpyClaw\nEliteAbilities=FIREPOWER,SELF_HEAL\n\n[ORCAB]\nElite=EliteBomb\nEliteAbilities=FIREPOWER,SELF_HEAL\n\n[SCRIN]\nElite=EliteProton\nEliteAbilities=FIREPOWER,SELF_HEAL\n\n[GAGATE_A]\nDeployTime=0.022\nGateCloseDelay=0.044\n\n[GAGATE_B]\nDeployTime=0.022\nGateCloseDelay=0.044\n\n[GAPAVE]\nAdjacent=5\n\n[GAPLUG2]\nPrerequisite=GAPLUG\n\n[GAPLUG3]\nPrerequisite=GAPLUG\n\n[NAGATE_A]\nDeployTime=0.022\nGateCloseDelay=0.044\n\n[NAGATE_B]\nDeployTime=0.022\nGateCloseDelay=0.044\n\n[NAOBEL]\nArmor=heavy\nStrength=600\n\n[NAWAST]\nAdjacent=5\nStrength=1200\n\n[NAPAVE]\nAdjacent=5\n\n[AssaultCannon]\nBurst=2\nDamage=31\n\n[CyCannon]\nSpeed=35\n\n[Heal]\nBurst=2\nRange=4.5\n\n[LaserFire]\nROF=46\n\n[ProtonBlast]\nRanged=no\nProximity=no\nAcceleration=12\n\n[Vulcan3]\nDamage=11\n\n[EliteAssaultCannon]\nROF=25\nBurst=2\nRange=6\nSpeed=100\nDamage=37\nReport=TSGUN4\nWarhead=SA\nProjectile=Invisible\n\n[EliteRaiderCannon]\nAnim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\nRange=4.8\nSpeed=100\nDamage=48\nReport=CHAINGN1\nWarhead=SA\nProjectile=Invisible\n\n[Elite120mm]\nROF=80\nAnim=GUNFIRE\nRange=8.1\nSpeed=90\nBright=yes\nDamage=84\nReport=120MMF\nWarhead=AP\nProjectile=Invisible\n\n[Elite155mm]\nROF=110\nAnim=GUNFIRE\nRange=21.60\nSpeed=10\nDamage=180\nLobber=yes\nReport=120MMF\nWarhead=ARTYHE\nProjectile=Ballistic\nMinimumRange=5.00\n\n[Elite90mm]\nROF=50\nAnim=GUNFIRE\nRange=8.1\nSpeed=40\nBright=yes\nDamage=43\nReport=120MMF\nWarhead=AP\nProjectile=Cannon\n\n[EliteBikeMissile]\nROF=60\nRange=6\nSpeed=30\nDamage=48\nReport=MISL1\nWarhead=AP\nProjectile=HeatSeeker\n\n[EliteBomb]\nROF=10\nRange=5\nSpeed=0\nDamage=192\nFloater=yes\nWarhead=ORCAHE\nProjectile=Cannon2\n\n[EliteCyCannon]\nROF=50\nRange=8.4\nSpeed=35\nDamage=180\nReport=scrin5b\nWarhead=PlasmaWH\nProjectile=ProtonBlast\n\n[EliteDragon]\nROF=50\nBurst=2\nRange=7.2\nSpeed=25\nDamage=36\nReport=MISL1\nWarhead=AP\nProjectile=AAHeatSeeker2\n\n[EliteFiendShard]\nROF=30\nBurst=3\nRange=6\nSpeed=25\nDamage=42\nReport=FIEND2\nWarhead=Shard\nProjectile=DogShard\n\n[EliteHarpyClaw]\nROF=36\nRange=5\nSpeed=100\nDamage=72\nReport=CYGUN1\nWarhead=SA\nProjectile=Invisible2\n\n[EliteHellfire]\nROF=50\nBurst=2\nRange=6\nSpeed=30\nDamage=36\nReport=ORCAMIS1\nWarhead=ORCAAP\nProjectile=AAHeatSeeker2\n\n[EliteHoverMissile]\nROF=68\nBurst=2\nRange=9.6\nSpeed=30\nDamage=36\nReport=HOVRMIS1\nWarhead=AP\nProjectile=AAHeatSeeker2\nMinimumRange=2\n\n[EliteLtRail]\nROF=60\nAnim=GUNFIRE\nRange=7.2\nSpeed=100\nDamage=0\nReport=BIGGGUN1\nWarhead=RailShot2\nIsRailgun=true\nProjectile=Invisible\nAmbientDamage=180\nAttachedParticleSystem=SmallRailgunSys\n\n[EliteMechRailgun]\nROF=60\nAnim=GUNFIRE\nRange=9.6\nSpeed=100\nDamage=0\nReport=RAILUSE5\nWarhead=RailShot\nIsRailgun=true\nProjectile=Invisible\nAmbientDamage=300\nAttachedParticleSystem=LargeRailgunSys\n\n[EliteProton]\nROF=3\nRange=5\nSpeed=30\nDamage=24\nReport=scrin5b\nWarhead=AP\nProjectile=ProtonTorpedo\n\n[EliteSlimeAttack]\nROF=80\nRange=1.56\nSpeed=25\nDamage=120\nReport=VICER1\nWarhead=Slimer\nProjectile=Invisible\n\n[ORCAHE]\nVerses=200%,90%,75%,32%,32%\n\n[IonWH]\nVerses=100%,90%,75%,60%,25%\n"
  },
  {
    "path": "DXMainClient/Resources/INI/MPMaps.ini",
    "content": "[MultiMaps]\n0=MAPS\\TIBERIAN SUN\\TERRACE\n1=MAPS\\FIRESTORM\\DUEL\n2=MAPS\\FAN-MADE\\PAIN_REDEFINED\n3=MAPS\\FAN-MADE\\COLD_WAR\n\n[GameModeAliases]\nStandard=Custom Map\nstandard=Custom Map\n\n[MAPS\\TIBERIAN SUN\\TERRACE]\nCD=0,1,2\n;MinPlayers=1\nMaxPlayers=4\nDescription=[4] Terrace\nAuthor=Westwood Studios\nEnforceMaxPlayers=False\nSize=0,0,120,120\nLocalSize=2,4,116,109\nPreviewSize=800,386\nGameModes=Default\nWaypoint0=118035\nWaypoint1=110188\nWaypoint2=199114\nWaypoint3=29130\n\n[MAPS\\FIRESTORM\\DUEL]\nCD=0,1,2\n;MinPlayers=1\nMaxPlayers=2\nDescription=[2] FS Dueling Islands\nAuthor=Westwood Studios\nEnforceMaxPlayers=False\nSize=0,0,88,69\nLocalSize=0,0,88,69\nPreviewSize=800,302\nGameModes=Default\nWaypoint0=107064\nWaypoint1=58083\n\n[MAPS\\FAN-MADE\\PAIN_REDEFINED]\nCD=0,1,2\n;MinPlayers=1\nMaxPlayers=2\nDescription=[2] Pain Redefined\nAuthor=Aro\nEnforceMaxPlayers=False\nSize=0,0,65,80\nLocalSize=2,4,61,69\nPreviewSize=800,482\nGameModes=Fan-made\nWaypoint0=111087\nWaypoint1=68031\n\n[MAPS\\FAN-MADE\\COLD_WAR]\nCD=0,1,2\n;MinPlayers=1\nMaxPlayers=3\nDescription=[3] Cold War\nAuthor=Aro\nEnforceMaxPlayers=False\nSize=0,0,75,88\nLocalSize=2,4,71,78\nPreviewSize=800,465\nGameModes=Fan-made\nWaypoint0=51047\nWaypoint1=136094\nWaypoint2=88128\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Difficulty Easy.ini",
    "content": "[Events]\nDifficultyGlobal=1,8,0,0\n\n[Actions]\nDifficultyGlobal=2,28,0,12,0,0,0,0,A,28,0,15,0,0,0,0,A\n\n[Triggers]\nDifficultyGlobal=Neutral,<none>,Set Difficulty Global,0,1,1,1,0\n\n[Tags]\nDifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Difficulty Hard.ini",
    "content": "[Events]\nDifficultyGlobal=1,8,0,0\n\n[Actions]\nDifficultyGlobal=2,28,0,14,0,0,0,0,A,28,0,16,0,0,0,0,A\n\n[Triggers]\nDifficultyGlobal=Neutral,<none>,Set Difficulty Global,0,1,1,1,0\n\n[Tags]\nDifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Difficulty Medium.ini",
    "content": "[Events]\nDifficultyGlobal=1,8,0,0\n\n[Actions]\nDifficultyGlobal=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\n\n[Triggers]\nDifficultyGlobal=Neutral,<none>,Set Difficulty Global,0,1,1,1,0\n\n[Tags]\nDifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/King of the Hill.ini",
    "content": "[Tags]\nShortGameTimerTag=0,Short Game Timer 1,ShortGameTimer\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Naval Only AI.ini",
    "content": "[Basic]\nIgnoreGlobalAITriggers=yes\n\n[General]\nMultiplayerAICM=2147483640,2147483640,2147483640\n\n[HPAD_AI]\nPrerequisite=NSYRD_AI\n\n[AHPAD_AI]\nPrerequisite=ASYRD_AI\n\n[GSYRD_AI]\nPrerequisite=none\n\n[NSYRD_AI]\nPrerequisite=none\n\n[ASYRD_AI]\nPrerequisite=none\n\n[RASPEN_AI]\nPrerequisite=none\n\n[AITriggerTypes]\n08507720-G=E_GDI replace MCV,084ECBA0-G,<all>,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,<none>,1,0,0\n091F4720-G=E_Nod replace MCV,084EC720-G,<all>,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,<none>,1,0,0\n08B80660-G=M_GDI replace MCV,084EC7FM-G,<all>,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,<none>,0,1,0\n0930A240-G=M_Nod replace MCV,084EC65M-G,<all>,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,<none>,0,1,0\n0930BDC0-G=H_Nod replace MCV,084EC650-G,<all>,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,<none>,0,0,1\n08B80760-G=H_GDI replace MCV,084EC7F0-G,<all>,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,<none>,0,0,1\n08507720-S=E_Soviet replace MCV,084ECBA0-S,<all>,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,<none>,1,0,0\n08B80660-S=M_Soviet replace MCV,084EC7FM-S,<all>,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,<none>,0,1,0\n08B80760-S=H_Soviet replace MCV,084EC7F0-S,<all>,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,<none>,0,0,1\n08507720-A=E_Allies replace MCV,084ECBA0-A,<all>,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,<none>,1,0,0\n08B80660-A=M_Allies replace MCV,084EC7FM-A,<all>,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,<none>,0,1,0\n08B80760-A=H_Allies replace MCV,084EC7F0-A,<all>,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,<none>,0,0,1\n012345678-G=MCV GDI Deploy,MCV_GDI_DEP,<all>,1,1,GFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,1,0,<none>,1,1,1\n012345678-N=MCV Nod Deploy,MCV_NOD_DEP,<all>,1,1,NFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,2,0,<none>,1,1,1\n012345678-A=MCV Allies Deploy,MCV_ALI_DEP,<all>,1,1,AFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,3,0,<none>,1,1,1\n012345678-S=MCV Soviet Deploy,MCV_SOV_DEP,<all>,1,1,SFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,4,0,<none>,1,1,1\n\n0859C820-G=E_GDI vehicle attack 2,080CF780-G,<all>,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF6B0-G,1,0,0\n0859C720-G=E_GDI vehicle attack 3,080CF520-G,<all>,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0\n09103C20-G=E_GDI hover pool,090EE280-G,<all>,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0\n08440AE0-G=E_GDI base air defense 5,084E5B50-G,<all>,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n0859C820-A=E_Allies vehicle attack 2,080CF780-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,080CF6B0-A,1,0,0\n0859C720-A=E_Allies vehicle attack 3,080CF520-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,080CF520-A,1,0,0\n09103C20-A=E_Allies hover pool,090EE280-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,090EE280-A,1,0,0\n08440AE0-A=E_Allies base air defense 5,084E5B50-A,<all>,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,3,0,084E5A80-A,1,0,0\n07CC6F10-A=E_Allies juggernaut pool,09E4FF40-A,<all>,3,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,09E4FF40-A,1,0,0\n090F8D20-A=H_Allies hover pool,090E0620-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,1,090E0620-A,0,0,1\n08596C20-A=H_Allies vehicle attack 2,0753BE90-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,0,073A8540-A,0,0,1\n08596B20-A=H_Allies vehicle attack 3,07EA0540-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,0,<none>,0,0,1\n084EF820-A=H_Allies ORCA bomber pool,0B7D44A0-A,<all>,5,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,3,1,<none>,0,0,1\n0859A160-A=H_Allies missile silo attack 1,084DE340-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080F71A0-A,0,0,1\n0859BE20-A=H_Allies missile silo attack 4,080CCD80-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CCCA0-A,0,0,1\n0859B330-A=H_Allies upgrade center attack 1,084E0F50-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CEA80-A,0,0,1\n0859CE20-A=H_Allies upgrade center attack 4,080CE620-A,<all>,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CE540-A,0,0,1\n084712B0-A=H_Allies power facility attack 1,073AE790-A,<all>,7,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,041742A0-A,0,0,1\n07CC6F10-S=E_Soviet juggernaut pool,09E4FF40-S,<all>,3,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,09E4FF40-S,1,0,0\n07CC6580-S=E_Soviet base defense attack 3,08820200-S,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,08820200-S,1,0,0\n07CC7F10-S=E_Soviet factories attack 7,09E4A280-S,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,09E4A280-S,1,0,0\n07CC7BE0-S=E_Soviet missile silo attack 5,09E4FD80-S,<all>,3,0,TMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,4,0,09E4FD80-S,1,0,0\n07CC78B0-S=E_Soviet power facilities attack 5,041A7750-S,<all>,3,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,041A7750-S,1,0,0\n07CC6800-S=E_Soviet upgrade center attack 5,07CC69F0-S,<all>,3,0,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,4,0,07CC69F0-S,1,0,0\n090F8D20-S=H_Soviet hover pool,090E0620-S,<all>,7,1,SWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,1,090E0620-S,0,0,1\n08596C20-S=H_Soviet vehicle attack 2,0753BE90-S,<all>,7,1,SWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,0,073A8540-S,0,0,1\n08596B20-S=H_Soviet vehicle attack 3,07EA0540-S,<all>,7,1,SWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,0,<none>,0,0,1\n080D1820-G=M_GDI hover pool,090E062M-G,<all>,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D3A20-G=M_GDI vehicle attack 2,0753BE9M-G,<all>,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073A854M-G,0,1,0\n\n085A22A0-G=E_Nod aerial base attack 1,09D00530-G,<all>,5,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,09D00530-G,1,0,0\n085A21A0-G=E_Nod aerial base attack 2,08607590-G,<all>,7,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,08607590-G,1,0,0\n085A20A0-G=E_Nod aerial vehicle attack ,086074C0-G,<all>,7,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086074C0-G,1,0,0\n080C71E0-G=M_GDI aerial harvester attack,0B7F742M-G,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080C71E1-G=M_GDI aerial harvester attack,0B7F742M-G,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080C71E2-G=M_GDI aerial harvester attack,0B7F742M-G,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080C71E3-G=M_GDI aerial harvester attack,0B7F742M-G,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080D1C20-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080D1C21-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080D1C22-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080D1C23-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,<none>,0,1,0\n080D2620-G=M_GDI aerial harvester attack 4,041768BM-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D2621-G=M_GDI aerial harvester attack 4,041768BM-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D2622-G=M_GDI aerial harvester attack 4,041768BM-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D2623-G=M_GDI aerial harvester attack 4,041768BM-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D2220-G=M_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D2221-G=M_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D2222-G=M_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D2223-G=M_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D4F24-G=M_Nod aerial base attack 1,0B7D48EM-G,<all>,5,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,30.000000,40.000000,1,0,2,0,<none>,0,1,0\n080D4E20-G=M_Nod aerial harvester attack 2,0B7D473M-G,<all>,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D4E21-G=M_Nod aerial harvester attack 2,0B7D473M-G,<all>,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D4E22-G=M_Nod aerial harvester attack 2,0B7D473M-G,<all>,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D4E23-G=M_Nod aerial harvester attack 2,0B7D473M-G,<all>,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D4D20-G=M_Nod aerial vehicle attack,0B7F750M-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,<none>,0,1,0\n084EFB20-G=H_Nod aerial base attack 1,0B7D48E0-G,<all>,5,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,50.000000,60.000000,1,0,2,0,<none>,0,0,1\n084EFA20-G=H_Nod aerial harvester attack 2,0B7D4730-G,<all>,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,<none>,0,0,1\n084EFA21-G=H_Nod aerial harvester attack 2,0B7D4730-G,<all>,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,<none>,0,0,1\n084EFA22-G=H_Nod aerial harvester attack 2,0B7D4730-G,<all>,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,<none>,0,0,1\n084EFA23-G=H_Nod aerial harvester attack 2,0B7D4730-G,<all>,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,<none>,0,0,1\n084F0B20-G=H_Nod aerial vehicle attack,0B7F7500-G,<all>,9,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,0,1\n084EFF20-G=H_GDI aerial harvester attack 1,0B7D4E70-G,<all>,5,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,<none>,0,0,1\n084EFF21-G=H_GDI aerial harvester attack 1,0B7D4E70-G,<all>,5,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,<none>,0,0,1\n084EFF22-G=H_GDI aerial harvester attack 1,0B7D4E70-G,<all>,5,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,<none>,0,0,1\n084EFF23-G=H_GDI aerial harvester attack 1,0B7D4E70-G,<all>,5,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,<none>,0,0,1\n084F0A20-G=H_GDI aerial harvester attack,0B7F7420-G,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,<none>,0,0,1\n084F0A21-G=H_GDI aerial harvester attack,0B7F7420-G,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,<none>,0,0,1\n084F0A22-G=H_GDI aerial harvester attack,0B7F7420-G,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,<none>,0,0,1\n084F0A23-G=H_GDI aerial harvester attack,0B7F7420-G,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,<none>,0,0,1\n085983D0-G=H_GDI aerial harvester attack 4,0B7F46F0-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1\n085983D1-G=H_GDI aerial harvester attack 4,0B7F46F0-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1\n085983D2-G=H_GDI aerial harvester attack 4,0B7F46F0-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1\n085983D3-G=H_GDI aerial harvester attack 4,0B7F46F0-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1\n075A52F0-G=H_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1\n075A52F1-G=H_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1\n075A52F2-G=H_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1\n075A52F3-G=H_GDI aerial harvester attack 4,04174D70-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1\n08472E20-G=H_GDI aerial harvester attack 4,041768B0-G,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1\n08472E21-G=H_GDI aerial harvester attack 4,041768B0-G,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1\n08472E22-G=H_GDI aerial harvester attack 4,041768B0-G,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1\n08472E23-G=H_GDI aerial harvester attack 4,041768B0-G,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1\n0859E270-S=E_Soviet aerial base attack 1,07F2A9F0-S,<all>,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,07F2A9F0-S,1,0,0\n0859E170-S=E_Soviet aerial base attack 2,07F2A920-S,<all>,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,07EF88B0-S,1,0,0\n0859E070-S=E_Soviet aerial vehicle attack,07F2A850-S,<all>,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,4,0,07F2A850-S,1,0,0\n080CF220-S=M_Soviet aerial base attack 1,0B7D4E7M-S,<all>,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,4,0,<none>,0,1,0\n080CF120-S=M_Soviet aerial base attack 2,0B7D4A9M-S,<all>,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,4,0,<none>,0,1,0\n080C71E0-S=M_Soviet aerial harvester attack,0B7F742M-S,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080C71E1-S=M_Soviet aerial harvester attack,0B7F742M-S,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080C71E2-S=M_Soviet aerial harvester attack,0B7F742M-S,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080C71E3-S=M_Soviet aerial harvester attack,0B7F742M-S,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080D1C20-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,<all>,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080D1C21-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,<all>,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080D1C22-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,<all>,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080D1C23-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,<all>,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,<none>,0,1,0\n080D2620-S=M_Soviet aerial harvester attack 4,041768BM-S,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,<none>,0,1,0\n080D2621-S=M_Soviet aerial harvester attack 4,041768BM-S,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,<none>,0,1,0\n080D2622-S=M_Soviet aerial harvester attack 4,041768BM-S,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,<none>,0,1,0\n080D2623-S=M_Soviet aerial harvester attack 4,041768BM-S,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,<none>,0,1,0\n080D2220-S=M_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,<none>,0,1,0\n080D2221-S=M_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,<none>,0,1,0\n080D2222-S=M_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,<none>,0,1,0\n080D2223-S=M_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,<none>,0,1,0\n084EFF20-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,<none>,0,0,1\n084EFF21-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,<none>,0,0,1\n084EFF22-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,<none>,0,0,1\n084EFF23-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,<none>,0,0,1\n084EFC20-S=H_Soviet aerial base attack 2,0B7D4A90-S,<all>,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,45.000000,35.000000,55.000000,1,0,4,0,<none>,0,0,1\n084F0A20-S=H_Soviet aerial harvester attack,0B7F7420-S,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,<none>,0,0,1\n084F0A21-S=H_Soviet aerial harvester attack,0B7F7420-S,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,<none>,0,0,1\n084F0A22-S=H_Soviet aerial harvester attack,0B7F7420-S,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,<none>,0,0,1\n084F0A23-S=H_Soviet aerial harvester attack,0B7F7420-S,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,<none>,0,0,1\n085983D0-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,<all>,8,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1\n085983D1-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,<all>,8,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1\n085983D2-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,<all>,8,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1\n085983D3-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,<all>,8,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1\n075A52F0-S=H_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1\n075A52F1-S=H_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1\n075A52F2-S=H_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1\n075A52F3-S=H_Soviet aerial harvester attack 4,04174D70-S,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1\n08472E20-S=H_Soviet aerial harvester attack 4,041768B0-S,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1\n08472E21-S=H_Soviet aerial harvester attack 4,041768B0-S,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1\n08472E22-S=H_Soviet aerial harvester attack 4,041768B0-S,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1\n08472E23-S=H_Soviet aerial harvester attack 4,041768B0-S,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1\n0859E270-A=E_Allies aerial base attack 1,07F2A9F0-A,<all>,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,3,0,07F2A9F0-A,1,0,0\n0859E170-A=E_Allies aerial base attack 2,07F2A920-A,<all>,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,3,0,07EF88B0-A,1,0,0\n0859E070-A=E_Allies aerial vehicle attack,07F2A850-A,<all>,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,3,0,07F2A850-A,1,0,0\n080CF220-A=M_Allies aerial base attack 1,0B7D4E7M-A,<all>,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,3,0,<none>,0,1,0\n080CF120-A=M_Allies aerial base attack 2,0B7D4A9M-A,<all>,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,3,0,<none>,0,1,0\n080C71E0-A=M_Allies aerial harvester attack,0B7F742M-A,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080C71E1-A=M_Allies aerial harvester attack,0B7F742M-A,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080C71E2-A=M_Allies aerial harvester attack,0B7F742M-A,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080C71E3-A=M_Allies aerial harvester attack,0B7F742M-A,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080D1C20-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,<all>,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080D1C21-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,<all>,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080D1C22-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,<all>,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080D1C23-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,<all>,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,<none>,0,1,0\n080D2620-A=M_Allies aerial harvester attack 4,041768BM-A,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,<none>,0,1,0\n080D2621-A=M_Allies aerial harvester attack 4,041768BM-A,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,<none>,0,1,0\n080D2622-A=M_Allies aerial harvester attack 4,041768BM-A,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,<none>,0,1,0\n080D2623-A=M_Allies aerial harvester attack 4,041768BM-A,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,<none>,0,1,0\n080D2220-A=M_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,<none>,0,1,0\n080D2221-A=M_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,<none>,0,1,0\n080D2222-A=M_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,<none>,0,1,0\n080D2223-A=M_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,<none>,0,1,0\n084EFF20-A=H_Allies aerial harvester attack 1,0B7D4E70-A,<all>,5,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,<none>,0,0,1\n084EFF21-A=H_Allies aerial harvester attack 1,0B7D4E70-A,<all>,5,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,<none>,0,0,1\n084EFF22-A=H_Allies aerial harvester attack 1,0B7D4E70-A,<all>,5,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,<none>,0,0,1\n084EFF23-A=H_Allies aerial harvester attack 1,0B7D4E70-A,<all>,5,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,<none>,0,0,1\n084EFC20-A=H_Allies aerial base attack 2,0B7D4A90-A,<all>,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,45.000000,35.000000,55.000000,1,0,3,0,<none>,0,0,1\n084F0A20-A=H_Allies aerial harvester attack,0B7F7420-A,<all>,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,<none>,0,0,1\n084F0A21-A=H_Allies aerial harvester attack,0B7F7420-A,<all>,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,<none>,0,0,1\n084F0A22-A=H_Allies aerial harvester attack,0B7F7420-A,<all>,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,<none>,0,0,1\n084F0A23-A=H_Allies aerial harvester attack,0B7F7420-A,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,<none>,0,0,1\n085983D0-A=H_Allies aerial harvester attack 4,0B7F46F0-A,<all>,8,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1\n085983D1-A=H_Allies aerial harvester attack 4,0B7F46F0-A,<all>,8,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1\n085983D2-A=H_Allies aerial harvester attack 4,0B7F46F0-A,<all>,8,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1\n085983D3-A=H_Allies aerial harvester attack 4,0B7F46F0-A,<all>,8,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1\n075A52F0-A=H_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1\n075A52F1-A=H_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1\n075A52F2-A=H_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1\n075A52F3-A=H_Allies aerial harvester attack 4,04174D70-A,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1\n08472E20-A=H_Allies aerial harvester attack 4,041768B0-A,<all>,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1\n08472E21-A=H_Allies aerial harvester attack 4,041768B0-A,<all>,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1\n08472E22-A=H_Allies aerial harvester attack 4,041768B0-A,<all>,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1\n08472E23-A=H_Allies aerial harvester attack 4,041768B0-A,<all>,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1\n\n; anti-naval yard triggers\nGDINAVAG-G=HM_GDI Aerial GDI Naval Yard Attack,GDIAANGT-G,<all>,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,<none>,0,1,1\n;GDINAVHG-G=HM_GDI Hovering GDI Naval Yard Attack,GDIHANAV-G,<all>,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,<none>,0,1,1\nNODNAVAG-G=H_Nod Aerial GDI Naval Yard Attack,NODAANVT-G,<all>,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,<none>,0,0,1\n;NODNAVHG-G=HM_Nod Hovering GDI Naval Yard Attack,NODHANVT-G,<all>,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,<none>,0,1,1\n\nGDINAVAN-G=HM_GDI Aerial Nod Naval Yard Attack,GDIAANNT-G,<all>,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,1,0,<none>,0,1,1\n;GDINAVHN-G=HM_GDI Hovering Nod Naval Yard Attack,GDIHANNT-G,<all>,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,<none>,0,1,1\nNODNAVAN-G=H_Nod Aerial Nod Naval Yard Attack,NODAANNT-G,<all>,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,2,0,<none>,0,0,1\n;NODNAVHN-G=HM_Nod Hovering Nod Naval Yard Attack,NODHANNT-G,<all>,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,<none>,0,1,1\n\nGDINAVAS-G=HM_GDI Aerial Soviet Naval Yard Attack,GDIAANST-G,<all>,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,1,0,<none>,0,1,1\n;GDINAVHS-G=HM_GDI Hovering Soviet Naval Yard Attack,GDIHANST-G,<all>,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,<none>,0,1,1\nNODNAVAS-G=H_Nod Aerial Soviet Naval Yard Attack,NODAANST-G,<all>,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,2,0,<none>,0,0,1\n;NODNAVHS-G=HM_Nod Hovering Soviet Naval Yard Attack,NODHANST-G,<all>,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,<none>,0,1,1\n\n; ship triggers\nNANVLASE-G=E_Nod Naval Patrol 1,NNVLASET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLABE-G=E_Nod Naval Building Attack 1,NNVLABET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLS2E-G=E_Nod Naval Patrol 2,NNVLASET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLB2E-G=E_Nod Naval Building Attack 2,NNVLABET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLS3E-G=E_Nod Naval Patrol 3,NNVLASET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLB3E-G=E_Nod Naval Building Attack 3,NNVLABET-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,<none>,1,0,0\nNANVLASM-G=M_Nod Naval Patrol 1,NNVLASMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,<none>,0,1,0\nNANVLABM-G=M_Nod Naval Building Attack,NNVLABMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,<none>,0,1,0\nNANVLS2M-G=M_Nod Naval Patrol 2,NNVLASMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,<none>,0,1,0\nNANVLB2M-G=M_Nod Naval Building Attack 2,NNVLABMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,<none>,0,1,0\nNANVLS3M-G=M_Nod Naval Patrol 3,NNVLASMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,<none>,0,1,0\nNANVLB3M-G=M_Nod Naval Building Attack 3,NNVLABMT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,<none>,0,1,0\nNANVLASH-G=H_Nod Naval Patrol 1,NNVLASHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,<none>,0,0,1\nNANVLABH-G=H_Nod Naval Building Attack 1,NNVLABHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,<none>,0,0,1\nNANVLS2H-G=H_Nod Naval Patrol 2,NNVLASHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,<none>,0,0,1\nNANVLB2H-G=H_Nod Naval Building Attack 2,NNVLABHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,<none>,0,0,1\nNANVLS3H-G=H_Nod Naval Patrol 3,NNVLASHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,<none>,0,0,1\nNANVLB3H-G=H_Nod Naval Building Attack 3,NNVLABHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,<none>,0,0,1\nNANVLS4H-G=H_Nod Naval Patrol 4,NNVLASHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,<none>,0,0,1\nNANVLB4H-G=H_Nod Naval Building Attack 4,NNVLABHT-G,<all>,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,<none>,0,0,1\nGANVLASE-G=E_GDI Naval Patrol 1,GNVLASET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLABE-G=E_GDI Naval Building Attack 1,GNVLABET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLS2E-G=E_GDI Naval Patrol 2,GNVLASET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLB2E-G=E_GDI Naval Building Attack 2,GNVLABET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLS3E-G=E_GDI Naval Patrol 3,GNVLASET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLB3E-G=E_GDI Naval Building Attack 3,GNVLABET-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,<none>,1,0,0\nGANVLASM-G=M_GDI Naval Patrol 1,GNVLASMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,<none>,0,1,0\nGANVLABM-G=M_GDI Naval Building Attack 1,GNVLABMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,<none>,0,1,0\nGANVLS2M-G=M_GDI Naval Patrol 2,GNVLASMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,<none>,0,1,0\nGANVLB2M-G=M_GDI Naval Building Attack 2,GNVLABMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,<none>,0,1,0\nGANVLS3M-G=M_GDI Naval Patrol 3,GNVLASMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,<none>,0,1,0\nGANVLB3M-G=M_GDI Naval Building Attack 3,GNVLABMT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,<none>,0,1,0\nGANVLASH-G=H_GDI Naval Patrol 1,GNVLASHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,<none>,0,0,1\nGANVLABH-G=H_GDI Naval Building Attack 1,GNVLABHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,<none>,0,0,1\nGANVLS2H-G=H_GDI Naval Patrol 2,GNVLASHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,<none>,0,0,1\nGANVLB2H-G=H_GDI Naval Building Attack 2,GNVLABHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,<none>,0,0,1\nGANVLS3H-G=H_GDI Naval Patrol 3,GNVLASHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,<none>,0,0,1\nGANVLB3H-G=H_GDI Naval Building Attack 3,GNVLABHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,<none>,0,0,1\nGANVLS4H-G=H_GDI Naval Patrol 4,GNVLASHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,<none>,0,0,1\nGANVLB4H-G=H_GDI Naval Building Attack 4,GNVLABHT-G,<all>,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,<none>,0,0,1\nSANVLASE-G=E_Soviet Naval Patrol 1,SNVLASET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLABE-G=E_Soviet Naval Building Attack 1,SNVLABET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLS2E-G=E_Soviet Naval Patrol 2,SNVLASET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLB2E-G=E_Soviet Naval Building Attack 2,SNVLABET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLS3E-G=E_Soviet Naval Patrol 3,SNVLASET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLB3E-G=E_Soviet Naval Building Attack 3,SNVLABET-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,<none>,1,0,0\nSANVLASM-G=M_Soviet Naval Patrol 1,SNVLASMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,<none>,0,1,0\nSANVLABM-G=M_Soviet Naval Building Attack 1,SNVLABMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,<none>,0,1,0\nSANVLS2M-G=M_Soviet Naval Patrol 2,SNVLASMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,<none>,0,1,0\nSANVLB2M-G=M_Soviet Naval Building Attack 2,SNVLABMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,<none>,0,1,0\nSANVLS3M-G=M_Soviet Naval Patrol 3,SNVLASMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,<none>,0,1,0\nSANVLB3M-G=M_Soviet Naval Building Attack 3,SNVLABMT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,<none>,0,1,0\nSANVLASH-G=H_Soviet Naval Patrol 1,SNVLASHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,<none>,0,0,1\nSANVLABH-G=H_Soviet Naval Building Attack 1,SNVLABHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,<none>,0,0,1\nSANVLS2H-G=H_Soviet Naval Patrol 2,SNVLASHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,<none>,0,0,1\nSANVLB2H-G=H_Soviet Naval Building Attack 2,SNVLABHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,<none>,0,0,1\nSANVLS3H-G=H_Soviet Naval Patrol 3,SNVLASHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,<none>,0,0,1\nSANVLB3H-G=H_Soviet Naval Building Attack 3,SNVLABHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,<none>,0,0,1\nSANVLS4H-G=H_Soviet Naval Patrol 4,SNVLASHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,<none>,0,0,1\nSANVLB4H-G=H_Soviet Naval Building Attack 4,SNVLABHT-G,<all>,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,<none>,0,0,1\nAANVLASE-G=E_Allies Naval Patrol 1,ANVLASET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLABE-G=E_Allies Naval Building Attack 1,ANVLABET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLS2E-G=E_Allies Naval Patrol 2,ANVLASET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLB2E-G=E_Allies Naval Building Attack 2,ANVLABET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLS3E-G=E_Allies Naval Patrol 3,ANVLASET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLB3E-G=E_Allies Naval Building Attack 3,ANVLABET-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,<none>,1,0,0\nAANVLASM-G=M_Allies Naval Patrol 1,ANVLASMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,<none>,0,1,0\nAANVLABM-G=M_Allies Naval Building Attack 1,ANVLABMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,<none>,0,1,0\nAANVLS2M-G=M_Allies Naval Patrol 2,ANVLASMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,<none>,0,1,0\nAANVLB2M-G=M_Allies Naval Building Attack 2,ANVLABMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,<none>,0,1,0\nAANVLS3M-G=M_Allies Naval Patrol 3,ANVLASMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,<none>,0,1,0\nAANVLB3M-G=M_Allies Naval Building Attack 3,ANVLABMT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,<none>,0,1,0\nAANVLASH-G=H_Allies Naval Patrol 1,ANVLASHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,<none>,0,0,1\nAANVLABH-G=H_Allies Naval Building Attack 1,ANVLABHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,<none>,0,0,1\nAANVLS2H-G=H_Allies Naval Patrol 2,ANVLASHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,<none>,0,0,1\nAANVLB2H-G=H_Allies Naval Building Attack 2,ANVLABHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,<none>,0,0,1\nAANVLS3H-G=H_Allies Naval Patrol 3,ANVLASHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,<none>,0,0,1\nAANVLB3H-G=H_Allies Naval Building Attack 3,ANVLABHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,<none>,0,0,1\nAANVLS4H-G=H_Allies Naval Patrol 4,ANVLASHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,<none>,0,0,1\nAANVLB4H-G=H_Allies Naval Building Attack 4,ANVLABHT-G,<all>,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,<none>,0,0,1\n\n;0TRMTHH0-G=H_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,<none>,0,0,1\n;0TRMTHH1-G=H_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,<none>,0,0,1\n;0TRMTHH2-G=H_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,<none>,0,0,1\n;0TRMTHH3-G=H_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,<none>,0,0,1\n;0TRMTFH1-G=H_Nod Termite Conyard Attack,TRMITF01-G,<all>,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,<none>,0,0,1\n;0TRMTFH2-G=H_Nod Termite Soviet Conyard Attack,TRMITF02-G,<all>,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,<none>,0,0,1\n;0TRMTFH3-G=H_Nod Termite Allied Conyard Attack,TRMITF03-G,<all>,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,<none>,0,0,1\n;0TRMTRH1-G=H_Nod Termite Refinery Attack,TRMITR01-G,<all>,-1,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,<none>,0,0,1\n;0TRMTRH2-G=H_Nod Termite Refinery Attack,TRMITR02-G,<all>,-1,0,RAPROC,0300000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,<none>,0,0,1\n;0TRMTMH0-G=M_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,<none>,0,1,0\n;0TRMTMH1-G=M_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,<none>,0,1,0\n;0TRMTMH2-G=M_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,<none>,0,1,0\n;0TRMTMH3-G=M_Nod Termite harvester attack,TRMITETT-G,<all>,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,<none>,0,1,0\n;0TRMTFM1-G=M_Nod Termite Conyard Attack,TRMITM01-G,<all>,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,<none>,0,1,0\n;0TRMTFM2-G=M_Nod Termite Conyard Attack,TRMITM02-G,<all>,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,<none>,0,1,0\n;0TRMTFM3-G=M_Nod Termite Conyard Attack,TRMITM03-G,<all>,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,<none>,0,1,0\n;0TRMTRM1-G=M_Nod Termite Refinery Attack,TRMTRM01-G,<all>,-1,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,130.000000,40.000000,130.000000,1,0,2,0,<none>,0,1,0\n;0TRMTRM2-G=M_Nod Termite Refinery Attack,TRMTRM02-G,<all>,-1,0,RAPROC,0300000003000000000000000000000000000000000000000000000000000000,130.000000,40.000000,130.000000,1,0,2,0,<none>,0,1,0\n;0TRMTEH0-G=E_Nod Termite harvester attack,TRMITEET-G,<all>,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,<none>,1,0,0\n;0TRMTEH1-G=E_Nod Termite harvester attack,TRMITEET-G,<all>,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,<none>,1,0,0\n;0TRMTEH2-G=E_Nod Termite harvester attack,TRMITEET-G,<all>,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,<none>,1,0,0\n;0TRMTEH3-G=E_Nod Termite harvester attack,TRMITEET-G,<all>,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,<none>,1,0,0\n;0TRMTRE1-G=E_Nod Termite Refinery Attack,TRMTRE01-G,<all>,-1,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,80.000000,20.000000,110.000000,1,0,2,0,<none>,0,1,0\n;0TRMTRE2-G=E_Nod Termite Refinery Attack,TRMTRE01-G,<all>,-1,0,RAPROC,0200000003000000000000000000000000000000000000000000000000000000,80.000000,20.000000,110.000000,1,0,2,0,<none>,0,1,0\n01A10CND-G=H_GDI A10 GDI Conyard Rush,8A10CNRD-G,<all>,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,<none>,0,0,1\n02A10CND-G=H_GDI A10 Nod Conyard Rush,8A10CNRN-G,<all>,-1,0,NFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,<none>,0,0,1\n03A10CND-G=H_GDI A10 Soviet Conyard Rush,8A10CNRS-G,<all>,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,<none>,0,0,1\n04A10CND-G=H_GDI A10 Allied Conyard Rush,8A10CNRA-G,<all>,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,<none>,0,0,1\n\n01BMTH0H-G=H_Soviet Behemoth Attack,BMTHTTH1-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,120.000000,20.000000,140.000000,1,0,4,0,<none>,0,0,1\n;02BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,<all>,-1,0,ARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;03BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,<all>,-1,0,RAARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;04BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,<all>,-1,0,AIARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;05BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,<all>,-1,0,AIRAARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;06BMTH0H-G=H_Soviet Behemoth V2 Counter,BMTHTTH1-G,<all>,-1,0,V2RL,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;07BMTH0H-G=H_Soviet Behemoth V2 Counter,BMTHTTH1-G,<all>,-1,0,AIV2RL,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;08BMTH0H-G=H_Soviet Behemoth Missile Launcher Counter,BMTHTTH1-G,<all>,-1,0,MSAM,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;09BMTH0H-G=H_Soviet Behemoth Missile Launcher Counter,BMTHTTH1-G,<all>,-1,0,AIMSAM,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,<none>,0,0,1\n;01BMTH0M-G=M_Soviet Behemoth Attack,BMTHTTM1-G,<all>,-1,1,RASTEK,0100000003000000000000000000000000000000000000000000000000000000,100.000000,10.000000,130.000000,1,0,4,0,<none>,0,1,0\n;02BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,<all>,-1,0,ARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;03BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,<all>,-1,0,RAARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;04BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,<all>,-1,0,AIARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;05BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,<all>,-1,0,AIRAARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;06BMTH0M-G=M_Soviet Behemoth V2 Counter,BMTHTTM1-G,<all>,-1,0,V2RL,0400000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;07BMTH0M-G=M_Soviet Behemoth V2 Counter,BMTHTTM1-G,<all>,-1,0,AIV2RL,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;08BMTH0H-G=M_Soviet Behemoth Missile Launcher Counter,BMTHTTM1-G,<all>,-1,0,MSAM,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;09BMTH0H-G=M_Soviet Behemoth Missile Launcher Counter,BMTHTTM1-G,<all>,-1,0,AIMSAM,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,<none>,0,1,0\n;01BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,<all>,-1,0,MISS,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,<none>,1,0,0\n;02BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,<all>,-1,0,TMPL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,<none>,1,0,0\n;03BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,<all>,-1,0,RAATEK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,<none>,1,0,0\n;04BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,<all>,-1,0,RASTEK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,<none>,1,0,0\n\n; fixed Classic AI triggers\n0859C720-G=E_GDI vehicle attack 3,080CF520-G,<all>,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0\n0859C3E0-G=E_GDI vehicle attack 7,080D0D90-G,<all>,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D0D90-G,1,0,0\n09103C20-G=E_GDI hover pool,090EE280-G,<all>,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0\n080D3520-G=M_GDI vehicle attack 7,07EA22FM-G,<all>,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D6520-G=M_Nod upgrade center attack 2,084E023M-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE1CM-G,0,1,0\n09101B20-G=E_Nod ranged pool,090ED230-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED230-G,1,0,0\n080D5220-G=M_Nod missile silo attack 2,084DF7EM-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CC92M-G,0,1,0\n09F2EA20-G=M_Nod base MLRS vehicle pool,07E7F0EM-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n085940B0-G=H_Nod base MLRS vehicle pool,07E7F0E0-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,<none>,0,0,1\n0859BB30-G=H_Nod missile silo attack 2,084DF7E0-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,0,1\n090EFC90-G=H_Nod ranged pool,0832E300-G,<all>,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E300-G,0,0,1\n\n; adjusted Classic GDI air attack triggers\n0859E270-G=E_GDI aerial base attack 1,07F2A9F0-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07F2A9F0-G,1,0,0\n0859E170-G=E_GDI aerial base attack 2,07F2A920-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07EF88B0-G,1,0,0\n0859E070-G=E_GDI aerial vehicle attack,07F2A850-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07F2A850-G,1,0,0\n085A0320-G=E_GDI ORCA fighter pool,084E4610-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,1,0,0\n080CF220-G=M_GDI aerial base attack 1,0B7D4E7M-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,1,0,<none>,0,1,0\n080CF120-G=M_GDI aerial base attack 2,0B7D4A9M-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,1,0,<none>,0,1,0\n0859F140-G=M_GDI ORCA fighter pool,0B7D458M-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n084EFC20-G=H_GDI aerial base attack 2,0B7D4A90-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,55.000000,35.000000,65.000000,1,0,1,0,<none>,0,0,1\n084EF920-G=H_GDI ORCA fighter pool,0B7D4580-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,<none>,0,0,1\n084EF820-G=H_GDI ORCA bomber pool,0B7D44A0-G,<all>,-1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,80.000000,80.000000,80.000000,1,0,1,1,<none>,0,0,1\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Scavenger.ini",
    "content": "[CrateRules]\nCrateMinimum=100\n\n[Powerups]\nArmor=33,ARMOR,0.75\nExplosion=0,RAPID,50\nFirepower=28,FIREPOWR,1.5\nHealBase=0,HEALALL\nICBM=10,CHEMISLE\nMoney=28,MONEY,200\nNapalm=0,<none>,500\nSpeed=30,SPEED,1.35\nUnit=35,<none>\nInvulnerability=15,AREAHEAL,1.0\nVeteran=0,VETERAN,1\nGas=0,<none>,100\nTiberium=0,<none>\nPod=0,MTRINIT\n\n[General]\nPrerequisitePower=BUILDCONST,PYLE_AI,HAND_AI,RATENT_AI,RABARR_AI\nPrerequisiteBarracks=BUILDCONST,PYLE_AI,HAND_AI,RATENT_AI,RABARR_AI\nGDIPowerPlant=TWR_AI,GUN_AI,RAPBOX_AI,RAFTUR_AI\nNodRegularPower=TWR_AI,GUN_AI,RAPBOX_AI,RAFTUR_AI\nNodAdvancedPower=GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI\n;BaseUnit=MHQ\nMultiplayerAICM=2147483640,2147483640,2147483640\n\n[AI]\nBuildPower=TWR_AI,GUN_AI,RAGUN_AI,RAFTUR_AI\nBuildDefense=TWR_AI,GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI\nBuildAA=GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI\nPowerSurplus=0\nDefenseLimit=10\nAARatio=0\nAALimit=0\n\n[BUILDCONST]\nRadar=yes\n\n; ******* Infantry Types *******\n[E4S]\nPrerequisite=RABARR\n\n[E5]\nPrerequisite=HAND\n\n[SHOK]\nPrerequisite=RABARR\n\n[ENGINEER]\nPrerequisite=CABHUT\nTechLevel=11\n\n[RMBO]\nTechLevel=11\n\n[CYP]\nTechLevel=11\n\n[TANYA]\nTechLevel=11\n\n[VOLKOV]\nTechLevel=11\n\n; ******* Vehicle Types *******\n[JEEP]\nPrerequisite=CABHUT\nOwner=GDI\nCrateGoodie=yes\n\n[RANG]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\n\n[BGGY]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\n\n[APC]\nPrerequisite=CABHUT\nOwner=GDI,Nod\nCrateGoodie=yes\n\n[RAAPC]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\n\n[SOVAPC]\nPrerequisite=CABHUT\nOwner=Soviet\nCrateGoodie=yes\n\n[BIKE]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\n\n[LTNK]\nOwner=Nod\n;CrateGoodie=yes\n\n[1TNK]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\n\n[MTNK]\nOwner=GDI\n;CrateGoodie=yes\n\n[2TNK]\nPrerequisite=AWEAP\nCost=800\nOwner=Allies\n;CrateGoodie=yes\n\n[FTNK]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\n\n[3TNK]\nOwner=Soviet\n;CrateGoodie=yes\n\n[HTNK]\nPrerequisite=CABHUT\nOwner=GDI\nCrateGoodie=yes\n\n[4TNK]\nPrerequisite=CABHUT\nOwner=Soviet\nCrateGoodie=yes\n\n[STNK]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\nCloakable=no\n\n[HVCT]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\n\n[DTRK]\nPrerequisite=CABHUT\nOwner=Soviet\nCrateGoodie=yes\n\n[MWAVE]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\n\n[TTNK]\nPrerequisite=CABHUT\nOwner=Soviet\nCrateGoodie=yes\n\n[ARTY]\nPrerequisite=CABHUT\nOwner=Nod\nCrateGoodie=yes\n\n[RAARTY]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\n\n[MSAM]\nPrerequisite=CABHUT\nOwner=GDI\nCrateGoodie=yes\n\n[MLRS]\nPrerequisite=CABHUT\nTechLevel=7\nOwner=Nod\nCrateGoodie=yes\n\n[V2RL]\nPrerequisite=CABHUT\nOwner=Soviet\nCrateGoodie=yes\n\n[PTNK]\nPrerequisite=CABHUT\nOwner=Allies\nCrateGoodie=yes\nCloakable=no\n\n[MRV]\nPrerequisite=CABHUT\n\n[MSA]\nTechLevel=11\n\n[CUMSA]\nCrateGoodie=no\n\n[HARV]\nCrateGoodie=no\n\n[TDHARV]\nCrateGoodie=no\n\n[RAHARV]\nCrateGoodie=no\n\n[FUHARV]\nCrateGoodie=no\n\n[FUTDHARV]\nCrateGoodie=no\n\n[FURAHARV]\nCrateGoodie=no\n\n[GMCV]\nPrerequisite=WEAP,FIX\nOwner=GDI\nCost=5000\n\n[NMCV]\nPrerequisite=AFLD,FIX\nOwner=Nod\nCost=5000\n\n[AMCV]\nPrerequisite=AWEAP,FIX\nOwner=Allies\nCost=5000\n\n[SMCV]\nPrerequisite=SWEAP,FIX\nOwner=Soviet\nCost=5000\n\n[SAPC]\nCrateGoodie=no\n\n[UTNK]\nCrateGoodie=no\n\n; ******* Building Types *******\n[BRIK]\nTechLevel=11\n\n[RABRIK]\nTechLevel=11\n\n[LSRPOST]\nTechLevel=11\n\n[GGATE_A]\nTechLevel=11\n\n[GGATE_B]\nTechLevel=11\n\n[NUKE]\nTechLevel=11\n\n[RAPOWR]\nTechLevel=11\n\n[PYLE]\nPrerequisite=GFACT\nPower=0\nAIBuildThis=no\n\n[HAND]\nPrerequisite=NFACT\nPower=0\n\n[RATENT]\nPrerequisite=AFACT\nPower=0\n\n[RABARR]\nPrerequisite=SFACT\nPower=0\n\n[PYLE_AI]\nAIBuildThis=yes\nPower=0\n\n[HAND_AI]\nPower=0\n\n[RATENT_AI]\nPower=0\n\n[RABARR_AI]\nPower=0\n\n[TWR]\nPrerequisite=GFACT\nPower=0\n\n[ATWR]\nPrerequisite=GFACT\nPower=0\n\n[RAPBOX]\nPrerequisite=AFACT\nPower=0\n\n[RAHBOX]\nPrerequisite=AFACT\nPower=0\n\n[GUN]\nPrerequisite=NFACT\nPower=0\n\n[RAGUN]\nPrerequisite=AFACT\nPrimary=GunCannon\nPower=0\n\n[RAFTUR]\nPrerequisite=SFACT\nPower=0\n\n[SAM]\nTechLevel=11\n\n[PROC]\nTechLevel=11\n\n[RAPROC]\nTechLevel=11\n\n[WEAP]\nPrerequisite=GFACT\nOwner=GDI,Civilian\nPower=0\n\n[AFLD]\nPrerequisite=NFACT\nOwner=Nod,Civilian\nPower=0\n\n[AWEAP]\nPrerequisite=AFACT\nOwner=Allies,Civilian\nPower=0\n\n[SWEAP]\nPrerequisite=SFACT\nOwner=Soviet,Civilian\nPower=0\n\n[WEAP_AI]\nPrerequisite=none\nBuildLimit=0\nWeaponsFactory=yes\nPower=0\nAIBuildThis=yes\n\n[AFLD_AI]\nPrerequisite=none\nBuildLimit=0\nWeaponsFactory=yes\nPower=0\nAIBuildThis=yes\n\n[AWEAP_AI]\nPrerequisite=none\nBuildLimit=0\nWeaponsFactory=yes\nPower=0\n\n[SWEAP_AI]\nPrerequisite=none\nBuildLimit=0\nWeaponsFactory=yes\nPower=0\n\n[FIX]\nPrerequisite=none\nOwner=GDI,Nod,Allies,Soviet\nPower=0\n\n[RAFIX]\nTechLevel=11\nPower=0\n\n[GSYRD]\nPrerequisite=CABHUT\nTechLevel=11\n\n[NSYRD]\nPrerequisite=CABHUT\nTechLevel=11\n\n[ASYRD]\nPrerequisite=CABHUT\nTechLevel=11\n\n[RASPEN]\nPrerequisite=CABHUT\nTechLevel=11\n\n[GSYRD_AI]\nPrerequisite=CABHUT\nTechLevel=11\n\n[NSYRD_AI]\nPrerequisite=CABHUT\nTechLevel=11\n\n[ASYRD_AI]\nPrerequisite=CABHUT\nTechLevel=11\n\n[RASPEN_AI]\nPrerequisite=CABHUT\nTechLevel=11\n\n[GHPAD]\nTechLevel=11\n\n[NHPAD]\nTechLevel=11\n\n[AHPAD]\nTechLevel=11\n\n[RAASTRP]\nTechLevel=11\n\n[NUKE_AI]\nAIBuildThis=no\n\n[NUK2_AI]\nAIBuildThis=no\n\n[NUK2A_AI]\nAIBuildThis=no\n\n[NUK2C_AI]\nAIBuildThis=no\n\n[RAPOWR_AI]\nAIBuildThis=no\n\n[PROC_AI]\nAIBuildThis=no\n\n[TWR_AI]\nPrerequisite=PYLE_AI\nPower=0\n\n[GUN_AI]\nPrerequisite=HAND_AI\nPower=0\n\n[RAPBOX_AI]\nPrerequisite=RATENT_AI\nPower=0\nAIBuildThis=no\n\n[RAFTUR_AI]\nPrerequisite=RABARR_AI\nPower=0\n\n[RAHBOX_AI]\nPrerequisite=RATENT_AI\nPower=0\n\n[RAGUN_AI]\nPrerequisite=RABARR_AI\nPrimary=GunCannon\nPower=0\n\n[ATWR_AI]\nPrerequisite=PYLE_AI\nPower=0\n\n[RATSLA_AI]\nTechLevel=11\n\n[TowerMissile]\nRange=7.2\n\n[TTankZap]\nDamage=100\nRange=8.4\n\n[Tiberiums]\n0=Riparius\n1=Cruentus\n2=Vinifera\n3=Aboreus\n\n[Riparius]\nName=Tiberium Riparius\nImage=1\nPower=40\nValue=25\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nColor=NeonGreen\n\n[Cruentus]\nName=Gems\nImage=2\nValue=60\nGrowth=10000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\nPower=19\nColor=DarkRed\nDebris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4\n\n[Vinifera]\nName=Tiberium Vinifera\nImage=3\nValue=40\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nPower=17\nColor=NeonBlue\nDebris=VINCRYS1,VINCRYS2,VINCRYS3,VINCRYS4\n\n[Aboreus]\nName=Ore\nImage=4\nValue=25\nGrowth=10000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\nPower=0\nColor=DarkGold\nDebris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4\n\n;[Tags]\n;ShortGameTimerTag=0,Short Game Timer 1,ShortGameTimer\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Map Code/Survivor.ini",
    "content": "[Events]\nStartingUnits1=1,13,0,0\nStartingUnits2=1,13,0,0\nStartingUnits3=1,13,0,0\nStartingUnits4=1,13,0,0\nStartingUnits5=1,13,0,0\nStartingUnits6=1,13,0,0\nStartingUnits7=1,13,0,0\nStartingUnits8=1,13,0,0\n\n[General]\nBaseUnit=MHQ\nHarvesterUnit=AITDHARV,AIRAHARV,MHQ\n\n[CrateRules]\nCrateMinimum=100\n\n[Powerups]\nArmor=33,ARMOR,0.75\nExplosion=0,RAPID,50\nFirepower=28,FIREPOWR,1.5\nHealBase=0,HEALALL\nICBM=10,CHEMISLE\nMoney=0,MONEY,100\nNapalm=0,<none>,500\nSpeed=30,SPEED,1.35\nUnit=75,<none>\nInvulnerability=15,AREAHEAL,1.0\nVeteran=0,VETERAN,1\nGas=0,<none>,100\nTiberium=0,<none>\nPod=0,MTRINIT\n\n; ******* Vehicle Types *******\n[JEEP]\nOwner=GDI\nCrateGoodie=yes\n\n[RANG]\nOwner=Allies\nCrateGoodie=yes\n\n[BGGY]\nOwner=Nod\nCrateGoodie=yes\n\n[APC]\nOwner=GDI,Nod\nCrateGoodie=yes\n\n[RAAPC]\nOwner=Allies\nCrateGoodie=yes\n\n[SOVAPC]\nOwner=Soviet\nCrateGoodie=yes\n\n[BIKE]\nOwner=Nod\nCrateGoodie=yes\n\n[LTNK]\nOwner=Nod\nCrateGoodie=yes\n\n[1TNK]\nOwner=Allies\nCrateGoodie=yes\n\n[MTNK]\nOwner=GDI\nCrateGoodie=yes\n\n[2TNK]\nCost=800\nOwner=Allies\nCrateGoodie=yes\n\n[FTNK]\nOwner=Nod\nCrateGoodie=yes\n\n[3TNK]\nOwner=Soviet\nCrateGoodie=yes\n\n[HTNK]\nOwner=GDI\nCrateGoodie=yes\n\n[4TNK]\nOwner=Soviet\nCrateGoodie=yes\n\n[STNK]\nOwner=Nod\nCrateGoodie=yes\nCloakable=no\n\n[HVCT]\nOwner=Allies\nCrateGoodie=yes\n\n[DTRK]\nOwner=Soviet\nCrateGoodie=yes\n\n[MWAVE]\nOwner=Nod\nCrateGoodie=yes\n\n[TTNK]\nOwner=Soviet\nCrateGoodie=yes\n\n[ARTY]\nOwner=Nod\nCrateGoodie=yes\n\n[RAARTY]\nOwner=Allies\nCrateGoodie=yes\n\n[MSAM]\nOwner=GDI\nCrateGoodie=yes\n\n[MLRS]\nTechLevel=7\nOwner=Nod\nCrateGoodie=yes\n\n[V2RL]\nOwner=Soviet\nCrateGoodie=yes\n\n[PTNK]\nOwner=Allies\nCrateGoodie=yes\nCloakable=no\n\n[MSA]\nTechLevel=11\n\n[CUMSA]\nCrateGoodie=no\n\n[HARV]\nCrateGoodie=no\n\n[TDHARV]\nCrateGoodie=no\n\n[RAHARV]\nCrateGoodie=no\n\n[FUHARV]\nCrateGoodie=no\n\n[FUTDHARV]\nCrateGoodie=no\n\n[FURAHARV]\nCrateGoodie=no\n\n\n[SAPC]\nCrateGoodie=no\n\n[UTNK]\nCrateGoodie=no\n\n[HUNTSEEK1]\nCrateGoodie=no\n\n[HUNTSEEK2]\nCrateGoodie=no\n\n[HUNTSEEK3]\nCrateGoodie=no\n\n[HUNTSEEK4]\nCrateGoodie=no\n\n[EMGMCV]\nImage=MTNKE\nName=Medium Tank\nCategory=AFV\nPrimary=90mmDummy\nSecondary=90mm\nStrength=400\nArmor=heavy\nTechLevel=3\t\nTurret=yes\nCrateGoodie=no\nSight=5\nSpeed=6\nOwner=GDI\nCost=800\nPoints=8\nROT=5\nExplosion=FRAGG1\nVoiceSelect=V1AWAIT,V1READY,V1REPORT,V1UNITRP\nVoiceMove=V1MOVOUT,V1ACKNO,V1AFFIRM,V1RAWAY,V1YESSIR\nVoiceAttack=V1ACKNO,V1AFFIRM,V1NOPROB,V1RAWAY,V1YESSIR\nVoiceFeedback=\nSpeedType=Track\nAccelerationFactor=0.04\nMovementZone=Destroyer\nThreatPosed=30\nDamageParticleSystems=SparkSys,SmallGreySSys\nTrainable=yes\nVeteranAbilities=STRONGER,SIGHT,FASTER\nEliteAbilities=SELF_HEAL\nElite=70mmMsl1\nAllowedToStartInMultiplayer=yes\n\n[EMNMCV]\nImage=LTNK\nName=Light Tank\nCategory=AFV\nPrerequisite=AFLD\nPrimary=75mm\nStrength=350\nArmor=heavy\nTechLevel=3\nTurret=yes\nCrateGoodie=no\nSight=5\nSpeed=6\nOwner=Nod\nCost=600\nPoints=6\nROT=13\nExplosion=FRAGG1\nVoiceSelect=V1AWAIT,V1READY,V1REPORT,V1UNITRP\nVoiceMove=V1MOVOUT,V1ACKNO,V1AFFIRM,V1RAWAY,V1YESSIR\nVoiceAttack=V1ACKNO,V1AFFIRM,V1NOPROB,V1RAWAY,V1YESSIR\nVoiceFeedback=\nSpeedType=Track\nAccelerationFactor=0.05\nMovementZone=Destroyer\nThreatPosed=25\nDamageParticleSystems=SparkSys,SmallGreySSys\nPipScale=Passengers\nPassengers=1\nTrainable=yes\nVeteranAbilities=STRONGER,SIGHT,FASTER\nEliteAbilities=SELF_HEAL\nElite=75mmE\nAllowedToStartInMultiplayer=yes\n\n[EMAMCV]\nImage=2TNK\nName=Medium Tank\nCategory=AFV\nPrimary=90mmRA\nStrength=400\nArmor=heavy\nTechLevel=3\nTurret=yes\nCrateGoodie=no\nSight=6\nSpeed=6\nOwner=Allies\nCost=800\nPoints=8\nROT=7\nExplosion=FRAGG1\nVoiceSelect=V5AWAIT,V5REPORT,V5VEHIC,V5YESSIR\nVoiceMove=V5ACKNO,V5AFFIRM\nVoiceAttack=V5ACKNO,V5AFFIRM\nSpeedType=Track\nAccelerationFactor=0.04\nMovementZone=Destroyer\nThreatPosed=30\nDamageParticleSystems=SparkSys,SmallGreySSys\nTrainable=yes\nVeteranAbilities=STRONGER,SIGHT,FASTER\nEliteAbilities=SELF_HEAL\nElite=90mmERA\nAllowedToStartInMultiplayer=yes\n\n[EMSMCV]\nImage=3TNK\nName=Heavy Tank\nCategory=AFV\nPrimary=105mm\nStrength=400\nArmor=heavy\nTechLevel=3\nTurret=yes\nCrateGoodie=no\nSight=6\nSpeed=5\nOwner=Soviet\nCost=950\nPoints=9\nROT=7\nExplosion=FRAGG1\nVoiceSelect=SV1AWAIT,SV1REPORT,SV1VEHIC,SV1YESSIR\nVoiceMove=SV1ACKNO,SV1AFFIRM\nVoiceAttack=SV1ACKNO,SV1AFFIRM\nSpeedType=Track\nAccelerationFactor=0.03\nMovementZone=Destroyer\nThreatPosed=30\nDamageParticleSystems=SparkSys,SmallGreySSys\nTrainable=yes\nVeteranAbilities=STRONGER,SIGHT,FASTER\nEliteAbilities=SELF_HEAL\nElite=105mmE\nAllowedToStartInMultiplayer=yes\n\n[TTankZap]\nDamage=100\nRange=8.4\n\n[Tiberiums]\n0=Riparius\n1=Cruentus\n2=Vinifera\n3=Aboreus\n\n[Riparius]\nName=Tiberium Riparius\nImage=1\nPower=40\nValue=25\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nColor=NeonGreen\n\n[Cruentus]\nName=Gems\nImage=2\nValue=60\nGrowth=10000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\nPower=19\nColor=DarkRed\nDebris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4\n\n[Vinifera]\nName=Tiberium Vinifera\nImage=3\nValue=40\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nPower=17\nColor=NeonBlue\nDebris=VINCRYS1,VINCRYS2,VINCRYS3,VINCRYS4\n\n[Aboreus]\nName=Ore\nImage=4\nValue=25\nGrowth=10000\nGrowthPercentage=10\nSpread=10000\nSpreadPercentage=0\nPower=0\nColor=DarkGold\nDebris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/MapSel.ini",
    "content": ";****************************************************************************\n;\n; FILE\n;     MapSel.ini\n;\n; DESCRIPTION\n;     This is a scenario progression control file.\n;\n; AUTHOR\n;     Denzil E. Long, Jr.\n;     $Author: $\n;\n; DATE\n;     November 11, 1998\n;     $Modtime: $\n;     $Revision: $\n;\n;****************************************************************************\n\n; GDI Progression Stages\n[GDI]\nAnims=Anims\nSounds=GDISFX\n1=GDI01   ;1A\n2=GDI02   ;2A\n3=GDI03   ;3A1\n4=GDI04   ;3A2\n5=GDI05   ;3B\n6=GDI06   ;4A1\n7=GDI07   ;4A2\n8=GDI08   ;5A1\n9=GDI09   ;5A2\n10=GDI10  ;5C\n11=GDI11  ;5C\n12=GDI12  ;5B\n13=GDI13  ;5B\n14=GDI14  ;6A\n15=GDI15  ;6A\n16=GDI16  ;6A\n17=GDI17  ;6A\n18=GDI18  ;6B\n19=GDI19  ;7A\n20=GDI20  ;8A -> 9A / 9B / 9D\n21=GDI21  ;9A -> 9B / 9D\n22=GDI22  ;9B -> 9D\n23=GDI23  ;9B -> 9C / 9D\n24=GDI24  ;9C\n25=GDI25  ;9D\n26=GDI26  ;10A\n27=GDI27  ;10B\n28=GDI28  ;11A\n29=GDI29  ;11A\n30=GDI30  ;12A\n\n; Sound effect entries\n; Event = Filename, Volume percentage\n[GDISFX]\nOverlay=GSWEEP.AUD,60\nTargetFlyIn=BESTBOX.AUD,75\n;MouseOnMap=MOUSEON.AUD,50\n;MouseOffMap=MOUSEOFF.AUD,100\nEnterRegion=EFFICIEN.AUD,40\nExitRegion=\nClickRegion=\n\n; NOD Progression Stages\n[Nod]\nAnims=Anims\nSounds=NODSFX\n1=NOD01   ;1A   - Start     - Leads to 2A\n2=NOD02   ;2A   - From 1A   - Leads to 3A1 or 3B\n3=NOD03   ;3A2  - From 2A   - Leads to 4A1 or 4B2\n4=NOD04   ;3A1  - From 3B   - Leads to 4A2 or 4B1\n5=NOD05   ;3B   - From 2A   - Leads to 3A1\n6=NOD06   ;4A2  - From 3A2  - Leads to 5A\n7=NOD07   ;4A1  - From 3A1  - Leads to 5A\n8=NOD08   ;4A4  - From 4B2  - Leads to 5A\n9=NOD09   ;4A3  - From 4B1  - Leads to 5A\n10=NOD10  ;4B2  - From 3A2  - Leads to 4A4\n11=NOD11  ;4B1  - From 3A1  - Leads to 4A3\n12=NOD12  ;5A   - From 4A4  - Leads to 6B or 6C\n13=NOD13  ;6B   - From 5A   - Leads to 6A1\n14=NOD14  ;6C   - From 5A   - Leads to 6A2\n15=NOD15  ;6A1  - From 6B   - Leads to 7A1 or 7B1\n16=NOD16  ;6A2  - From 6C   - Leads to 7A2 or 7B2\n17=NOD17  ;7A1  - From 6A1  - Leads to 8A\n18=NOD18  ;7A2  - From 6A2  - Leads to 8A\n19=NOD19  ;7A3  - From 7B1  - Leads to 8A\n20=NOD20  ;7A4  - From 7B2  - Leads to 8A\n21=NOD21  ;7B1  - From 6A1  - Leads to 7A3\n22=NOD22  ;7B2  - From 6A2  - Leads to 7A4\n23=NOD23  ;8A   - From 7Ax  - Leads to 9A or 9B\n24=NOD24  ;9A   - From 8A   - Leads to 10A1\n25=NOD25  ;9B   - From 8A   - Leads to 10A2\n26=NOD26  ;10A1 - From 9A   - Leads to 11A1\n27=NOD27  ;10A2 - From 9B   - Leads to 11A2\n28=NOD28  ;11A1 - From 10A1 - Leads to 12A1 or 12B1\n29=NOD29  ;11A2 - From 10A2 - Leads to 12A2 or 12B2\n30=NOD30  ;12B1 - From 11A1 - Leads to 12A\n31=NOD31  ;12B2 - From 11A2 - Leads to 12A\n32=NOD32  ;12A  - From 11A or 12B - Finish\n\n[NODSFX]\nOverlay = NSWEEP.AUD, 60\nTargetFlyIn = BESTBOX.AUD, 75\n;MouseOnMap = MOUSEON.AUD, 50\n;MouseOffMap = MOUSEOFF.AUD, 100\nEnterRegion = EFFICIEN.AUD, 40\nExitRegion =\nClickRegion =\n\n;****************************************************************************\n; Animations\n;\n; Format: Name, X, Y, Rate\n;****************************************************************************\n[Anims]\nTextRect=92,322,332,78\nPalette=MapSel.pal\n1=SMLOGO.SHP,16,322,5\n2=GLOBE.SHP,545,168,5\n3=COMPASS.SHP,448,255,5\n\n\n;****************************************************************************\n; PROGRESSION FIELDS\n;\n; Scenario  - Name of scenario to play for this stage\n;\n; Description - Text to display when mouse moves onto clickable region\n;\n; Text1...n - Text to display (Format: X,Y,Time,String)\n;             X,Y    - Display coordinate\n;             Time   - Time to display text, represented in ticks\n;                      (1/60th second) from start of presentation\n;             String - String to display\n;\n; MapVQ    - The map VQA to play\n;\n; Overlays - Overlays that fade up over the last frame of the MapVQ movie\n;\n; ClickMap - A 256 color PCX file (same resolution as the MapVQ) that\n;            identifies clickable regions. Each clickable region is\n;            identified by a unique color ranging from 1 - 255, color 0\n;            is considered background and is ignored. The numbered entries\n;            reflect the stage represented by the color in the clickmap.\n;\n; Targets  - Fly-in target positioning. Format: n,x,y,x,y... where 'n' is\n;            the number of targets.\n;****************************************************************************\n\n; Leads to 1A\n[GDI00]\nScenario=\nDescription=\nVoiceOver=\nMapVQ=GDIMAP01.VQA\nOverlays=RG01A.SHP,RN01A.SHP\nTargets=1,144,70\nClickMap=GDICLK01.PCX\n1=GDI01 ;1A\n\n; 1A - Leads to 2A\n[GDI01]\nScenario=Maps\\Missions\\GDI1A.MAP\nDescription=768\nVoiceOver=GDI-01.AUD\nMapVQ=GDIMAP01.VQA\nOverlays=RG02A.SHP,RN02A.SHP\nTargets=1,180,80\nClickMap=GDICLK01.PCX\n2=GDI02 ;2A\n\n;2A - Leads to 3A2 or 3B\n[GDI02]\nScenario=Maps\\Missions\\GDI2A.MAP\nDescription=769\nVoiceOver=GDI-02.AUD\nMapVQ=GDIMAP01.VQA\nOverlays=RG03AB.SHP,RN03AB.SHP\nTargets=2,290,88,218,108\nClickMap=GDICLK01.PCX\n3=GDI04 ;3A2\n4=GDI05 ;3B\n\n;3A1 - Leads to 4A1\n[GDI03]\nScenario=Maps\\Missions\\GDI3A.MAP\nDescription=770\nVoiceOver=GDI-03A.AUD\nMapVQ=GDIMAP01.VQA\nOverlays=RG04A1.SHP,RN04A1.SHP\nTargets=1,360,78\nClickMap=GDICLK01.PCX\n5=GDI06 ;4A1\n\n;3A2 - Leads to 4A2\n[GDI04]\nScenario=Maps\\Missions\\GDI3A.MAP\nDescription=770\nVoiceOver=GDI-03A.AUD\nMapVQ=GDIMAP01.VQA\nOverlays=RG04A2.SHP,RN04A2.SHP\nTargets=1,360,78\nClickMap=GDICLK01.PCX\n5=GDI07 ;4A2\n\n;3B - Leads to 3A1\n[GDI05]\nScenario=Maps\\Missions\\GDI3B.MAP\nDescription=771\nVoiceOver=GDI-03B.AUD\nMapVQ=GDIMAP01.VQA\nOverlays=RG03A.SHP,RN03A.SHP\nTargets=1,290,88\nClickMap=GDICLK01.PCX\n3=GDI03 ;3A1\n\n;4A1 - Leads to 5A1 or 5B1\n[GDI06]\nScenario=Maps\\Missions\\GDI4A.MAP\nDescription=772\nVoiceOver=GDI-04.AUD\nMapVQ=GDIMAP02.VQA\nOverlays=RG05AB1.SHP,RN05AB1.SHP\nTargets=2,188,183,280,256\nClickMap=GDICLK02.PCX\n6=GDI08 ;5A1\n7=GDI10 ;5C1\n\n;4A2 - Leads to 5A2 or 5B2\n[GDI07]\nScenario=Maps\\Missions\\GDI4A.MAP\nDescription=772\nVoiceOver=GDI-04.AUD\nMapVQ=GDIMAP02.VQA\nOverlays=RG05AB2.SHP,RN05AB2.SHP\nTargets=2,188,183,280,256\nClickMap=GDICLK02.PCX\n6=GDI09\t;5A2\n7=GDI11\t;5C2\n\n;5A1 - Leads to 5B1\n[GDI08]\nScenario=Maps\\Missions\\GDI5A.MAP\nDescription=773\nVoiceOver=GDI-05A.AUD\nMapVQ=GDIMAP02.VQA\nOverlays=RG05B1.SHP,RN05B1.SHP\nTargets=1,280,256\nClickMap=GDICLK02.PCX\n7=GDI12\n\n[GDI09]\nScenario=Maps\\Missions\\GDI5A.MAP\nDescription=773\nVoiceOver=GDI-05A.AUD\nMapVQ=GDIMAP02.VQA\nOverlays=RG05B2.SHP,RN05B2.SHP\nTargets=1,280,256\nClickMap=GDICLK02.PCX\n7=GDI13\n\n;5B1 - Leads to \n[GDI10]\nScenario=Maps\\Missions\\GDI5C.MAP\nDescription=774\nVoiceOver=GDI-05B.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06AB2.SHP,RN06AB2.SHP\nTargets=2,218,192,300,230\nClickMap=GDICLK03.PCX\n8=GDI16\n9=GDI18\n\n[GDI11]\nScenario=Maps\\Missions\\GDI5C.MAP\nDescription=774\nVoiceOver=GDI-05B.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06AB4.SHP,RN06AB4.SHP\nTargets=2,218,192,300,230\nClickMap=GDICLK03.PCX\n8=GDI17\n9=GDI18\n\n[GDI12]\nScenario=Maps\\Missions\\GDI5B.MAP\nDescription=774\nVoiceOver=GDI-05B.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06AB1.SHP,RN06AB1.SHP\nTargets=2,218,192,300,230\nClickMap=GDICLK03.PCX\n8=GDI14\n9=GDI18\n\n[GDI13]\nScenario=Maps\\Missions\\GDI5B.MAP\nDescription=774\nVoiceOver=GDI-05B.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06AB3.SHP,RN06AB3.SHP\nTargets=2,218,192,300,230\nClickMap=GDICLK03.PCX\n8=GDI15\n9=GDI18\n\n[GDI14]\nScenario=Maps\\Missions\\GDI6A.MAP\nDescription=775\nVoiceOver=GDI-06A.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06B1.SHP,RN06B1.SHP\nTargets=1,300,230\nClickMap=GDICLK03.PCX\n9=GDI18\n\n[GDI15]\nScenario=Maps\\Missions\\GDI6A.MAP\nDescription=775\nVoiceOver=GDI-06A.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06B3.SHP,RN06B3.SHP\nTargets=1,300,230\nClickMap=GDICLK03.PCX\n9=GDI18\n\n[GDI16]\nScenario=Maps\\Missions\\GDI6A.MAP\nDescription=775\nVoiceOver=GDI-06A.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06B2.SHP,RN06B2.SHP\nTargets=1,300,230\nClickMap=GDICLK03.PCX\n9=GDI18\n\n[GDI17]\nScenario=Maps\\Missions\\GDI6A.MAP\nDescription=775\nVoiceOver=GDI-06A.AUD\nMapVQ=GDIMAP03.VQA\nOverlays=RG06B4.SHP,RN06B4.SHP\nTargets=1,300,230\nClickMap=GDICLK03.PCX\n9=GDI18\n\n[GDI18]\nScenario=Maps\\Missions\\GDI6B.MAP\nDescription=776\nVoiceOver=GDI-06B.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG07A.SHP,RN07A.SHP\nTargets=1,272,32\nClickMap=GDICLK04.PCX\n10=GDI19\n\n[GDI19]\nScenario=Maps\\Missions\\GDI7A.MAP\nDescription=777\nVoiceOver=GDI-07.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG08A.SHP,RN08A.SHP\nTargets=1,168,154\nClickMap=GDICLK04.PCX\n11=GDI20\n\n[GDI20]\nScenario=Maps\\Missions\\GDI8A.MAP\nDescription=778\nVoiceOver=GDI-08.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG09ABD.SHP,RN09ABD.SHP\nTargets=3,116,274,82,190,64,242\nClickMap=GDICLK04.PCX\n12=GDI21 ;9A\n13=GDI22 ;9B -> 9D\n15=GDI25 ;9D\n\n;9A\n[GDI21]\nScenario=Maps\\Missions\\GDI9A.MAP\nDescription=779\nVoiceOver=GDI-09A.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG09BD.SHP,RN09BD.SHP\nTargets=2,82,190,64,242\nClickMap=GDICLK04.PCX\n13=GDI23 ;9B\n15=GDI25 ;9D\n\n;9B -> 9D\n[GDI22]\nScenario=Maps\\Missions\\GDI9B.MAP\nDescription=780\nVoiceOver=GDI-09B.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG09D1.SHP,RN09D1.SHP\nTargets=1,64,242\nClickMap=GDICLK04.PCX\n15=GDI25 ;9D\n\n;9B -> 9C / 9D\n[GDI23]\nScenario=Maps\\Missions\\GDI9B.MAP\nDescription=780\nVoiceOver=GDI-09B.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG09CD.SHP,RN09CD.SHP\nTargets=2,110,228,64,242\nClickMap=GDICLK04.PCX\n14=GDI24 ;9C\n15=GDI25 ;9D\n\n;9C\n[GDI24]\nScenario=Maps\\Missions\\GDI9C.MAP\nDescription=781\nVoiceOver=GDI-09C.AUD\nMapVQ=GDIMAP04.VQA\nOverlays=RG09D2.SHP,RN09D2.SHP\nTargets=1,64,242\nClickMap=GDICLK04.PCX\n15=GDI25 ;9D\n\n;9D\n[GDI25]\nScenario=Maps\\Missions\\GDI9D.MAP\nDescription=782\nVoiceOver=GDI-09D.AUD\nMapVQ=GDIMAP05.VQA\nOverlays=RG10AB1.SHP,RN10AB1.SHP\nTargets=2,160,72,206,30\nClickMap=GDICLK05.PCX\n16=GDI26 ;10A\n17=GDI27 ;10B\n\n[GDI26]\nScenario=Maps\\Missions\\GDI10A.MAP\nDescription=783\nVoiceOver=GDI-10A.AUD\nMapVQ=GDIMAP05.VQA\nOverlays=RG11A1.SHP,RN11A1.SHP\nTargets=1,230,148\nClickMap=GDICLK05.PCX\n18=GDI28\n\n[GDI27]\nScenario=Maps\\Missions\\GDI10B.MAP\nDescription=784\nVoiceOver=GDI-10B.AUD\nMapVQ=GDIMAP05.VQA\nOverlays=RG11A2.SHP,RN11A2.SHP\nTargets=1,230,148\nClickMap=GDICLK05.PCX\n18=GDI29\n\n\n[GDI28]\nScenario=Maps\\Missions\\GDI11A.MAP\nDescription=785\nVoiceOver=GDI-11.AUD\nMapVQ=GDIMAP05.VQA\nOverlays=RG12A1.SHP,RN12A1.SHP\nTargets=1,282,252\nClickMap=GDICLK05.PCX\n19=GDI30\n\n\n[GDI29]\nScenario=Maps\\Missions\\GDI11A.MAP\nDescription=785\nVoiceOver=GDI-11.AUD\nMapVQ=GDIMAP05.VQA\nOverlays=RG12A2.SHP,RN12A2.SHP\nTargets=1,282,252\nClickMap=GDICLK05.PCX\n19=GDI30\n\n\n[GDI30]\nScenario=Maps\\Missions\\GDI12A.MAP\nDescription=786\nVoiceOver=GDI-12.AUD\n\n\n;****************************************************************************\n; NOD STAGES\n;****************************************************************************\n\n;Leads to 1A\n[NOD00]\nScenario=Maps\\Missions\\\nDescription=\nVoiceOver=\nMapVQ=NODMAP01.VQA\nOverlays=TN01A.SHP,TG01A.SHP\nTargets=1,120,140\nClickMap=NODCLK01.PCX\n1=NOD01\t;1A\n\n;1A - Leads to 2A\n[NOD01]\nScenario=Maps\\Missions\\NOD1A.MAP\nDescription=787\nVoiceOver=NOD-01.AUD\nMapVQ=NODMAP01.VQA\nOverlays=TN02A.SHP,TG02A.SHP\nTargets=1,190,100\nClickMap=NODCLK01.PCX\n2=NOD02\t;2A\n\n;2A - Leads to 3A2 or 3B\n[NOD02]\nScenario=Maps\\Missions\\NOD2A.MAP\nDescription=788\nVoiceOver=NOD-02.AUD\nMapVQ=NODMAP01.VQA\nOverlays=TN03AB.SHP,TG03AB.SHP\nTargets=2,388,168,300,204\nClickMap=NODCLK01.PCX\n3=NOD03\t;3A2\n4=NOD05\t;3B\n\n;3A2 - Leads to 4A2 or 4B2\n[NOD03]\nScenario=Maps\\Missions\\NOD3A.MAP\nDescription=789\nVoiceOver=NOD-03A.AUD\nMapVQ=NODMAP02.VQA\nOverlays=TN04AB2.SHP,TG04AB2.SHP\nTargets=2,244,40,272,96\nClickMap=NODCLK02.PCX\n5=NOD06\t;4A2\n6=NOD10\t;4B2\n\n;3A1 - Leads to 4A1 or 4B1\n[NOD04]\nScenario=Maps\\Missions\\NOD3A.MAP\nDescription=789\nVoiceOver=NOD-03A.AUD\nMapVQ=NODMAP02.VQA\nOverlays=TN04AB1.SHP,TG04AB1.SHP\nTargets=2,244,40,272,96\nClickMap=NODCLK02.PCX\n5=NOD07\t;4A1\n6=NOD11\t;4B1\n\n;3B - Leads to 3A1\n[NOD05]\nScenario=Maps\\Missions\\NOD3B.MAP\nDescription=790\nVoiceOver=NOD-03B.AUD\nMapVQ=NODMAP01.VQA\nOverlays=TN03A.SHP,TG03A.SHP\nTargets=1,388,168\nClickMap=NODCLK01.PCX\n3=NOD04\t;3A1\n\n;4A2 - Leads to 5A\n[NOD06]\nScenario=Maps\\Missions\\NOD4A.MAP\nDescription=791\nVoiceOver=NOD-04A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN05A.SHP,TG05A.SHP\nTargets=1,206,138\nClickMap=NODCLK03.PCX\n7=NOD12\t;5A\n\n;4A1 - Leads to 5A\n[NOD07]\nScenario=Maps\\Missions\\NOD4A.MAP\nDescription=791\nVoiceOver=NOD-04A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN05A.SHP,TG05A.SHP\nTargets=1,206,138\nClickMap=NODCLK03.PCX\n7=NOD12\t;5A\n\n;4A4 - Leads to 5A\n[NOD08]\nScenario=Maps\\Missions\\NOD4A.MAP\nDescription=791\nVoiceOver=NOD-04A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN05A.SHP,TG05A.SHP\nTargets=1,206,138\nClickMap=NODCLK03.PCX\n7=NOD12\t;5A\n\n;4A3 - Leads to 5A\n[NOD09]\nScenario=Maps\\Missions\\NOD4A.MAP\nDescription=791\nVoiceOver=NOD-04A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN05A.SHP,TG05A.SHP\nTargets=1,206,138\nClickMap=NODCLK03.PCX\n7=NOD12\t;5A\n\n;4B2 - Leads to 4A4\n[NOD10]\nScenario=Maps\\Missions\\NOD4B.MAP\nDescription=854\nVoiceOver=NOD-04B.AUD\nMapVQ=NODMAP02.VQA\nOverlays=TN04A2.SHP,TG04A2.SHP\nTargets=1,244,40\nClickMap=NODCLK02.PCX\n5=NOD08\t;4A4\n\n;4B1 - Leads to 4A3\n[NOD11]\nScenario=Maps\\Missions\\NOD4B.MAP\nDescription=854\nVoiceOver=NOD-04B.AUD\nMapVQ=NODMAP02.VQA\nOverlays=TN04A1.SHP,TG04A1.SHP\nTargets=1,244,40\nClickMap=NODCLK02.PCX\n5=NOD09\t;4A3\n\n;5A - Leads to 6B or 6C\n[NOD12]\nScenario=Maps\\Missions\\NOD5A.MAP\nDescription=792\nVoiceOver=NOD-05.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN06BC.SHP,TG06BC.SHP\nTargets=2,148,34,122,128\nClickMap=NODCLK03.PCX\n9=NOD13\t;6B\n10=NOD14\t;6C\n\n;6B - Leads to 6A1\n[NOD13]\nScenario=Maps\\Missions\\NOD6B.MAP\nDescription=794\nVoiceOver=NOD-06B.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN06A1.SHP,TG06A1.SHP\nTargets=1,72,66\nClickMap=NODCLK03.PCX\n8=NOD15\t;6A1\n\n;6C - Leads to 6A2\n[NOD14]\nScenario=Maps\\Missions\\NOD6C.MAP\nDescription=795\nVoiceOver=NOD-06C.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN06A2.SHP,TG06A2.SHP\nTargets=1,72,66\nClickMap=NODCLK03.PCX\n8=NOD16\t;6A2\n\n;6A1 - Leads to 7A1 or 7B1\n[NOD15]\nScenario=Maps\\Missions\\NOD6A.MAP\nDescription=793\nVoiceOver=NOD-06A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN07AB1.SHP,TG07AB1.SHP\nTargets=2,328,260,302,170\nClickMap=NODCLK03.PCX\n11=NOD17\t;7A1\n12=NOD21\t;7B1\n\n;6A2 - Leads to 7A2 or 7B2\n[NOD16]\nScenario=Maps\\Missions\\NOD6A.MAP\nDescription=793\nVoiceOver=NOD-06A.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN07AB2.SHP,TG07AB2.SHP\nTargets=2,328,260,302,170\nClickMap=NODCLK03.PCX\n11=NOD18\t;7A2\n12=NOD22\t;7B2\n\n;7A1 - Leads to 8A\n[NOD17]\nScenario=Maps\\Missions\\NOD7A.MAP\nDescription=796\nVoiceOver=NOD-07A.AUD\nMapVQ=NODMAP04.VQA\nOverlays=TN08A.SHP,TG08A.SHP\nTargets=1,316,112\nClickMap=NODCLK04.PCX\n13=NOD23\t;8A\n\n;7A2 - Leads to 8A\n[NOD18]\nScenario=Maps\\Missions\\NOD7A.MAP\nDescription=796\nVoiceOver=NOD-07A.AUD\nMapVQ=NODMAP04.VQA\nOverlays=TN08A.SHP,TG08A.SHP\nTargets=1,316,112\nClickMap=NODCLK04.PCX\n13=NOD23\t;8A\n\n;7A3 - Leads to 8A\n[NOD19]\nScenario=Maps\\Missions\\NOD7A.MAP\nDescription=796\nVoiceOver=NOD-07A.AUD\nMapVQ=NODMAP04.VQA\nOverlays=TN08A.SHP,TG08A.SHP\nTargets=1,316,112\nClickMap=NODCLK04.PCX\n13=NOD23\t;8A\n\n;7A4 - Leads to 8A\n[NOD20]\nScenario=Maps\\Missions\\NOD7A.MAP\nDescription=796\nVoiceOver=NOD-07A.AUD\nMapVQ=NODMAP04.VQA\nOverlays=TN08A.SHP,TG08A.SHP\nTargets=1,316,112\nClickMap=NODCLK04.PCX\n13=NOD23\t;8A\n\n;7B1 - Leads to 7A3\n[NOD21]\nScenario=Maps\\Missions\\NOD7B.MAP\nDescription=797\nVoiceOver=NOD-07B.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN07A1.SHP,TG07A1.SHP\nTargets=1,328,260\nClickMap=NODCLK03.PCX\n11=NOD19\t;7A3\n\n;7B2 - Leads to 7A4\n[NOD22]\nScenario=Maps\\Missions\\NOD7B.MAP\nDescription=797\nVoiceOver=NOD-07B.AUD\nMapVQ=NODMAP03.VQA\nOverlays=TN07A2.SHP,TG07A2.SHP\nTargets=1,328,260\nClickMap=NODCLK03.PCX\n11=NOD20\t;7A4\n\n;8A - Leads to 9A or 9B\n[NOD23]\nScenario=Maps\\Missions\\NOD8A.MAP\nDescription=798\nVoiceOver=NOD-08.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN09AB.SHP,TG09AB.SHP\nTargets=2,202,262,218,212\nClickMap=NODCLK05.PCX\n14=NOD24\t;9A\n15=NOD25\t;9B\n\n;9A - Leads to 10A1\n[NOD24]\nScenario=Maps\\Missions\\NOD9A.MAP\nDescription=799\nVoiceOver=NOD-09A.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN10A1.SHP,TG10A1.SHP\nTargets=1,114,238\nClickMap=NODCLK05.PCX\n16=NOD26\t;10A1\n\n;9B - Leads to 10A2\n[NOD25]\nScenario=Maps\\Missions\\NOD9B.MAP\nDescription=800\nVoiceOver=NOD-09B.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN10A2.SHP,TG10A2.SHP\nTargets=1,114,238\nClickMap=NODCLK05.PCX\n16=NOD27\t;10A2\n\n;10A1 - Leads to 11A1\n[NOD26]\nScenario=Maps\\Missions\\NOD10A.MAP\nDescription=801\nVoiceOver=NOD-10.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN11A1.SHP,TG11A1.SHP\nTargets=1,362,96\nClickMap=NODCLK05.PCX\n17=NOD28\t;11A1\n\n;10A2 - Leads to 11A2\n[NOD27]\nScenario=Maps\\Missions\\NOD10A.MAP\nDescription=801\nVoiceOver=NOD-10.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN11A2.SHP,TG11A2.SHP\nTargets=1,362,96\nClickMap=NODCLK05.PCX\n17=NOD29\t;11A2\n\n;11A1 - Leads to 12A1 or 12B1\n[NOD28]\nScenario=Maps\\Missions\\NOD11A.MAP\nDescription=802\nVoiceOver=NOD-11.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN12AB1.SHP,TG12AB1.SHP\nTargets=2,422,34,472,84\nClickMap=NODCLK05.PCX\n18=NOD32\t;12A\n19=NOD30\t;12B1\n\n;11A2 - Leads to 12A2 or 12B2\n[NOD29]\nScenario=Maps\\Missions\\NOD11A.MAP\nDescription=802\nVoiceOver=NOD-11.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN12AB2.SHP,TG12AB2.SHP\nTargets=2,422,34,472,84\nClickMap=NODCLK05.PCX\n18=NOD32\t;12A\n19=NOD31\t;12B2\n\n;12B1 - Leads to 12A\n[NOD30]\nScenario=Maps\\Missions\\NOD12B.MAP\nDescription=804\nVoiceOver=NOD-12B.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN12A1.SHP,TG12A1.SHP\nTargets=1,422,34\nClickMap=NODCLK05.PCX\n18=NOD32\t;12A\n\n;12B2 - Leads to 12A\n[NOD31]\nScenario=Maps\\Missions\\NOD12B.MAP\nDescription=804\nVoiceOver=NOD-12B.AUD\nMapVQ=NODMAP05.VQA\nOverlays=TN12A2.SHP,TG12A2.SHP\nTargets=1,422,34\nClickMap=NODCLK05.PCX\n18=NOD32\t;12A\n\n;12A - Finish\n[NOD32]\nScenario=Maps\\Missions\\NOD12A.MAP\nDescription=803\nVoiceOver=NOD-12A.AUD\n"
  },
  {
    "path": "DXMainClient/Resources/INI/MapSel01.ini",
    "content": ";****************************************************************************\n;\n; FILE\n;     MapSel01.ini\n;\n; DESCRIPTION\n;     Scenario progression control file for Firestorm\n;\n; AUTHOR\n;     Denzil E. Long, Jr.\n;\n; DATE\n;     November 9, 1999\n;\n;****************************************************************************\n\n; GDI Progression Stages\n[GDI]\nAnims=Anims\nSounds=GDISFX\n1=FSGDI01\n2=FSGDI02\n3=FSGDI03\n4=FSGDI04\n5=FSGDI05\n6=FSGDI06\n7=FSGDI07\n8=FSGDI08\n9=FSGDI09\n\n; Sound effect entries\n; Event = Filename, Volume percentage\n[GDISFX]\nTargetFlyIn=BESTBOX.AUD,75\nEnterRegion=EFFICIEN.AUD,40\n\n\n; NOD Progression Stages\n[Nod]\nAnims=Anims\nSounds=NODSFX\n1=FSNOD01\n2=FSNOD02\n3=FSNOD03\n4=FSNOD04\n5=FSNOD05\n6=FSNOD06\n7=FSNOD07\n8=FSNOD08\n9=FSNOD09\n\n[NODSFX]\nTargetFlyIn = BESTBOX.AUD, 75\nEnterRegion = EFFICIEN.AUD, 40\n\n;****************************************************************************\n; Animations\n;\n; Format: Name, X, Y, Rate\n;****************************************************************************\n[Anims]\nTextRect=92,322,332,78\nPalette=MapSel.pal\n1=SMLOGO.SHP,16,322,5\n2=GLOBE.SHP,545,168,5\n3=COMPASS.SHP,448,255,5\n\n\n;****************************************************************************\n; PROGRESSION FIELDS\n;\n; Scenario  - Name of scenario to play for this stage\n;\n; Description - Text to display when mouse moves onto clickable region\n;\n; Text1...n - Text to display (Format: X,Y,Time,String)\n;             X,Y    - Display coordinate\n;             Time   - Time to display text, represented in ticks\n;                      (1/60th second) from start of presentation\n;             String - String to display\n;\n; VoiceOver - Audio file to play when mouse enters click region\n;\n; MapVQ    - The map VQA to play\n;\n; Overlays - Overlays that fade up over the last frame of the MapVQ movie\n;\n; ClickMap - A 256 color PCX file (same resolution as the MapVQ) that\n;            identifies clickable regions. Each clickable region is\n;            identified by a unique color ranging from 1 - 255 (Color 0\n;            is considered background and is ignored). The numbered entries\n;            reflect the stage represented by the color in the clickmap.\n;\n; Targets  - Fly-in target positioning. Format: n,x,y,x,y... where 'n' is\n;            the number of targets.\n;****************************************************************************\n\n; 1 leads to 2\n[FSGDI01]\nMapVQ=FSGMAP02.VQA\nTargets=1,314,128\nClickMap=FSGCLK02.PCX\n2=FSGDI02\n\n\n; 2 leads to 3\n[FSGDI02]\nScenario=Maps\\Missions\\FSGDI02.MAP\nDescription=GDIBRIEF02\nMapVQ=FSGMAP03.VQA\nTargets=1,214,128\nClickMap=FSGCLK03.PCX\n3=FSGDI03\n\n[GDIBRIEF02]\n1=We have lost communication with a small nearby civilian settlement.\n2=Their last message spoke of strange monsters attacking them. We do not have\n3=time to wait for a larger force and must investigate. Protect the civilians\n4=at all costs. Evacuate as many civilians as possible and get them to the\n5=pickup zone for immediate air transport.\n\n\n; 3 leads to 4\n[FSGDI03]\nScenario=Maps\\Missions\\FSGDI03.MAP\nDescription=GDIBRIEF03\nMapVQ=FSGMAP04.VQA\nTargets=1,324,156\nClickMap=FSGCLK04.PCX\n4=FSGDI04\n\n[GDIBRIEF03]\n1=The death of Tratos has caused open revolt among the mutants. For some reason\n2=they believe that the local food and water supplies have been poisoned, and\n3=are attacking the local depot. This has upset the civilians in the area,\n4=causing armed conflict between the two factions. Quell the rioting and prevent\n5=needless deaths and damage on BOTH sides of the conflict. To this end we have\n6=equipped your infantry with non-lethal weaponry. In addition you must prevent\n7=the destruction of the depot, as it supplies all of the relocated civilians and\n8=mutants in the area.\n\n\n; 4 leads to 5\n[FSGDI04]\nScenario=Maps\\Missions\\FSGDI04.MAP\nDescription=GDIBRIEF04\nMapVQ=FSGMAP05.VQA\nTargets=1,250,124\nClickMap=FSGCLK05.PCX\n5=FSGDI05\n\n[GDIBRIEF04]\n1=We believe CABAL's core to be in this area. Neutralize the two bridges to cut\n2=off enemy reinforcements. Capture CABAL using an engineer. Nod is using\n3=self-powered laser fencing to keep intruders away from the core. There should\n4=be command stations that you can capture to disable this fencing. Finally, deal\n5=with any remaining defenses guarding that core.\n\n\n; 5 leads to 6\n[FSGDI05]\nScenario=Maps\\Missions\\FSGDI05.MAP\nDescription=GDIBRIEF05\nMapVQ=FSGMAP06.VQA\nTargets=1,330,118\nClickMap=FSGCLK06.PCX\n6=FSGDI06\n\n[GDIBRIEF05]\n1=The second Tacitus piece is in an ancient temple located outside of La Paz,\n2=Bolivia. Locate the temple and retrieve the Tacitus. Be cautious, as the\n3=area is completely uncharted.\n\n\n; 6 leads to 7\n[FSGDI06]\nScenario=Maps\\Missions\\FSGDI06.MAP\nDescription=GDIBRIEF06\nMapVQ=FSGMAP07.VQA\nTargets=1,300,138\nClickMap=FSGCLK07.PCX\n7=FSGDI07\n\n[GDIBRIEF06]\n1=CABAL has betrayed us. GDI and perhaps the Earth itself are doomed unless\n2=we can call back and regroup enough to send for help. Our first priority\n3=is to get Dr. Boudreau to the relative safety of a nearby GDI outpost.\n4=Once she is safe, we can call for reinforcements and hopefully remove at\n5=least this part of CABAL's forces.\n\n\n; 7 leads to 8\n[FSGDI07]\nScenario=Maps\\Missions\\FSGDI07.MAP\nDescription=GDIBRIEF07\nMapVQ=FSGMAP08.VQA\nTargets=1,330,104\nClickMap=FSGCLK08.PCX\n8=FSGDI08\n\n[GDIBRIEF07]\n1=We've lost communication with our base outside of Trondheim. Get in there\n2=and find out what's happening.\n\n\n; 8 leads to 9\n[FSGDI08]\nScenario=Maps\\Missions\\FSGDI08.MAP\nDescription=GDIBRIEF08\nMapVQ=FSGMAP09.VQA\nTargets=1,200,126\nClickMap=FSGCLK09.PCX\n9=FSGDI09\n\n[GDIBRIEF08]\n1=Our scientists have reprogrammed a cyborg given to us by Nod forces.\n2=Carried within its internal circuitry is a virus, which it will release\n3=into CABAL's communications network. The cyborg must be inserted into the\n4=defensive outpost that lies between our forces and the cyborg creation\n5=plant. The lives of many civilians are at stake, and CABAL knows we are\n6=coming. The longer it takes to establish your base the more heavily he\n7=will be defended. GOOD LUCK!\n\n\n; 9 Ends the game\n[FSGDI09]\nScenario=Maps\\Missions\\FSGDI09.MAP\nDescription=GDIBRIEF09\n\n[GDIBRIEF09]\n1=Take CABAL down fast and hard. No mercy and no surrender. Find a way to\n2=get in there and take the core out.\n\n\n\n;****************************************************************************\n; NOD STAGES\n;****************************************************************************\n\n; 1 leads to 2\n[FSNOD01]\nMapVQ=FSNMAP02.VQA\nTargets=1,312,128\nClickMap=FSNCLK02.PCX\n2=FSNOD02\n\n\n; 2 leads to 3\n[FSNOD02]\nScenario=Maps\\Missions\\FSNOD02.MAP\nDescription=NODBRIEF02\nMapVQ=FSNMAP03.VQA\nTargets=1,272,102\nClickMap=FSNCLK03.PCX\n3=FSNOD03\n\n[NODBRIEF02]\n1=The first step in our Tiberium evolution requires the fertilization of\n2=the land with new indigenous life forms. We will use this new life to\n3=educate those who wish to interfere with its progress. Establish your\n4=base near the Genesis Pit. It is here that you will find the seeds of\n5=evolution. Lure the life forms out of their womb and to the feeding\n6=grounds. A nearby civilian settlement will serve as bait.\n\n\n; 3 leads to 4\n[FSNOD03]\nScenario=Maps\\Missions\\FSNOD03.MAP\nDescription=NODBRIEF03\nMapVQ=FSNMAP04.VQA\nTargets=1,220,134\nClickMap=FSNCLK04.PCX\n4=FSNOD04\n\n[NODBRIEF03]\n1=While GDI's forces have been diverted towards defending the civilians,\n2=you are to lead an elite strike force in an assassination operation against\n3=the leader of the mutants. Locate Tratos within the base. Our new Limpet\n4=mines will help to do this. Once located you must devise a way to reach and\n5=kill him. GDI will still have considerable protection for Tratos, as he is\n6=their last hope at defeating the Tiberium onslaught. We know that he will\n7=have mutant guardians, sensor arrays and GDI will have active firestorm walls\n8=set-up throughout the base. Destroying their power supply should neutralize\n9=the firestorm, an airstrike will deal with the sensor arrays and the rest is\n10=up to you. Do not fail as this mission is integral to the future of Nod.\n\n\n; 4 leads to 5\n[FSNOD04]\nScenario=Maps\\Missions\\FSNOD04.MAP\nDescription=NODBRIEF04\nMapVQ=FSNMAP05.VQA\nTargets=1,378,80\nClickMap=FSNCLK05.PCX\n5=FSNOD05\n\n[NODBRIEF04]\n1=The mutant vermin have once again made themselves known. They have stolen the\n2=Tacitus that I...we have worked so hard to obtain. If Kane's work is to be completed,\n3=we must recover it. Find the mutant encampment and recover the Tacitus. Once it is\n4=safely removed, terminate all mutants in the area. Perhaps this will teach them not\n5=to interfere with us again.\n\n\n; 5 leads to 6\n[FSNOD05]\nScenario=Maps\\Missions\\FSNOD05.MAP\nDescription=NODBRIEF05\nMapVQ=FSNMAP06.VQA\nTargets=1,324,66\nClickMap=FSNCLK06.PCX\n6=FSNOD06\n\n[NODBRIEF05]\n1=CABAL has betrayed us all. We must escape to regroup and repay his treachery.\n2=There is an abandoned airfield nearby; if we can reach it, we have a chance.\n3=Once we are there, we must repair the array to contact our forces and call for\n4=an evac. We have no information or tactical support now that CABAL has gone\n5=rogue, so we are on our own.\n\n\n; 6 leads to 7\n[FSNOD06]\nScenario=Maps\\Missions\\FSNOD06.MAP\nDescription=NODBRIEF06\nMapVQ=FSNMAP07.VQA\nTargets=1,276,60\nClickMap=FSNCLK07.PCX\n7=FSNOD07\n\n[NODBRIEF06]\n1=Since CABAL has turned on us we are suffering a communications blackout. We are\n2=forced to try and obtain GDI's EVA technology. There is a small GDI airbase in\n3=this sector. Get your engineer into their radar to steal an EVA unit. You may want\n4=to consider trying to create a distraction to otherwise preoccupy the GDI air units.\n5=Also, we have a new unit for you, the Mobile Stealth Generator. Use it wisely.\n\n\n; 7 leads to 8\n[FSNOD07]\nScenario=Maps\\Missions\\FSNOD07.MAP\nDescription=NODBRIEF07\nMapVQ=FSNMAP08.VQA\nTargets=1,328,124\nClickMap=FSNCLK08.PCX\n8=FSNOD08\n\n[NODBRIEF07]\n1=Scorched earth, plain and simple. Destroy all cybernetic forces in the area,\n2=the base and CABAL's computer core.\n\n\n; 8 leads to 9\n[FSNOD08]\nScenario=Maps\\Missions\\FSNOD08.MAP\nDescription=NODBRIEF08\nMapVQ=FSNMAP09.VQA\nTargets=1,198,130\nClickMap=FSNCLK09.PCX\n9=FSNOD09\n\n[NODBRIEF08]\n1=Prior to our main assault on CABAL we will need to slow down his production\n2=capabilities. CABAL is currently harvesting Tiberium heavily in Eastern Africa.\n3=Get in there and eliminate CABAL's harvesting abilities. Unfortunately, at this\n4=time we can only afford to provide you with a small strike force, use them wisely.\n\n\n; 9 Ends the game\n[FSNOD09]\nScenario=Maps\\Missions\\FSNOD09.MAP\nDescription=NODBRIEF09\n\n[NODBRIEF09]\n1=Take CABAL down fast and hard. No mercy and no surrender. Find a way to get in\n2=there and take the core out.\n"
  },
  {
    "path": "DXMainClient/Resources/INI/Menu.ini",
    "content": "[MainMenu]\nBackground=DTABackX\t;DTAbacks\n;Theme=Intro\n;0=ClassicMenuItem\n;1=EnhancedMenuItem\n2=MainMenuExit\n3=Escape\n;4=Version\n;5=Credits\n;6=VersionText\nItemMax=6\n\n[ClassicMenuItem]\nType=Image\nID=100\nImage=CDTAC\nHighlighted=CDTACH\nHighlightSound=BUTTON.AUD\nOrigin=196,158\nActiveRect=196,158,250,18\n\n[EnhancedMenuItem]\nType=Image\nID=101\nImage=CDTAE\nHighlighted=CDTAEH\nHighlightSound=BUTTON.AUD\nOrigin=196,184\nActiveRect=196,184,250,18\n\n[Credits]\nType=Image\t;Shortcut\nID=12\nImage=DTA10c\nHighlighted=DTA10c-h\nHighlightSound=BUTTON.AUD\nOrigin=196,210\nActiveRect=196,210,121,18\n;Keys=C\n\n[MainMenuExit]\nType=Image\nID=0\nImage=DTA9e\t;CDTAEX\nHighlighted=DTA9e-h\t;CDTAEXH\nHighlightSound=BUTTON.AUD\nOrigin=260,195\t;324,210\nActiveRect=260,195,121,18\t;324,210,121,18\n\n[EnhancedMenu]\nBackground=DTABACK\nTheme=Intro1\n0=EnhancedNewCampaign\n1=EnhancedLoadMission\n2=EnhancedLAN\n3=EnhancedInternet\n;4=EnhancedSerialModem\n5=EnhancedSkirmish\n6=EnhancedOptions\n7=EnhancedBack\n8=EnhancedExit\n9=Version\n10=EscapeBack\n11=Back\n12=EnhancedText\nItemMax=16\n\n[EnhancedNewCampaign]\nType=Image\nID=1\nImage=DTA1nc\nHighlighted=DTA1nc-h\nHighlightSound=BUTTON.AUD\nOrigin=196,118\nActiveRect=196,118,250,18\n\n[EnhancedLoadMission]\nType=Image\nID=2\nImage=DTA2lm\nHighlighted=DTA2lm-h\nHighlightSound=BUTTON.AUD\nDisabled=DTA2lm-g\nOrigin=196,144\nActiveRect=196,144,250,18\n\n[EnhancedLAN]\nType=Image\nID=3\nImage=DTA3l\nHighlighted=DTA3l-h\nHighlightSound=BUTTON.AUD\nOrigin=196,170\nActiveRect=196,170,250,18\n\n[EnhancedInternet]\nType=Image\nID=4\nImage=DTA4i\nHighlighted=DTA4i-h\nHighlightSound=BUTTON.AUD\nOrigin=196,196\nActiveRect=196,196,250,18\n\n[EnhancedSerialModem]\nType=Image\nID=5\nImage=DTA5sm\nHighlighted=DTA5sm-h\nHighlightSound=BUTTON.AUD\nOrigin=196,196\nActiveRect=196,196,250,18\n\n[EnhancedSkirmish]\nType=Image\nID=6\nImage=DTA6s\nHighlighted=DTA6s-h\nHighlightSound=BUTTON.AUD\nOrigin=196,222\nActiveRect=196,222,250,18\n\n[EnhancedOptions]\nType=Image\nID=8\nImage=DTA7o\nHighlighted=DTA7o-h\nHighlightSound=BUTTON.AUD\nOrigin=196,248\nActiveRect=196,248,250,18\n\n[EnhancedBack]\nType=Image\nID=102\nImage=DTA8b\nHighlighted=DTA8b-h\nHighlightSound=BUTTON.AUD\nOrigin=196,274\nActiveRect=196,274,121,18\n\n[EnhancedExit]\nType=Image\nID=0\nImage=DTA9e\nHighlighted=DTA9e-h\nHighlightSound=BUTTON.AUD\nOrigin=324,274\nActiveRect=324,274,121,18\n\n[EnhancedText]\nType=Image\nID=0\nImage=DTAEB\t;DTAE\nOrigin=226,27\t;226,7\t;293,58\nActiveRect=0,0,0,0\n\n[ClassicMenu]\nBackground=DTABACK\nTheme=Intro\n0=ClassicNewCampaign\n1=ClassicLoadMission\n2=ClassicLAN\n3=ClassicInternet\n;4=ClassicSerialModem\n5=ClassicSkirmish\n6=ClassicOptions\n7=ClassicBack\n8=ClassicExit\n9=Version\n10=EscapeBack\n11=Back\n12=ClassicText\nItemMax=15\n\n[ClassicNewCampaign]\nType=Image\nID=1\nImage=DTA1nc\nHighlighted=DTA1nc-h\nHighlightSound=BUTTON.AUD\nOrigin=196,118\nActiveRect=196,118,250,18\n\n[ClassicLoadMission]\nType=Image\nID=2\nImage=DTA2lm\nHighlighted=DTA2lm-h\nHighlightSound=BUTTON.AUD\nDisabled=DTA2lm-g\nOrigin=196,144\nActiveRect=196,144,250,18\n\n[ClassicLAN]\nType=Image\nID=3\nImage=DTA3l\nHighlighted=DTA3l-h\nHighlightSound=BUTTON.AUD\nOrigin=196,170\nActiveRect=196,170,250,18\n\n[ClassicInternet]\nType=Image\nID=4\nImage=DTA4i\nHighlighted=DTA4i-h\nHighlightSound=BUTTON.AUD\nOrigin=196,196\nActiveRect=196,196,250,18\n\n[ClassicSerialModem]\nType=Image\nID=5\nImage=DTA5sm\nHighlighted=DTA5sm-h\nHighlightSound=BUTTON.AUD\nOrigin=196,196\nActiveRect=196,196,250,18\n\n[ClassicSkirmish]\nType=Image\nID=6\nImage=DTA6s\nHighlighted=DTA6s-h\nHighlightSound=BUTTON.AUD\nOrigin=196,222\nActiveRect=196,222,250,18\n\n[ClassicOptions]\nType=Image\nID=8\nImage=DTA7o\nHighlighted=DTA7o-h\nHighlightSound=BUTTON.AUD\nOrigin=196,248\nActiveRect=196,248,250,18\n\n[ClassicBack]\nType=Image\nID=102\nImage=DTA8b\nHighlighted=DTA8b-h\nHighlightSound=BUTTON.AUD\nOrigin=196,274\nActiveRect=196,274,121,18\n\n[ClassicExit]\nType=Image\nID=0\nImage=DTA9e\nHighlighted=DTA9e-h\nHighlightSound=BUTTON.AUD\nOrigin=324,274\nActiveRect=324,274,121,18\n\n;[Version]\n;Type=Shortcut\n;ID=11\n;Keys=ctrl-v\n\n[Escape]\nType=Shortcut\nID=0\nKeys=ESC\n\n[Back]\nType=Shortcut\nID=102\nKeys=BACKSPACE\n\n[EscapeBack]\nType=Shortcut\nID=102\nKeys=ESC\n\n[ClassicText]\nType=Image\nID=0\nImage=DTACB\t;DTAC\nOrigin=254,26\t;254,6\t;293,58\nActiveRect=0,0,0,0\n\n;[VersionText]\n;Type=Version\n;ID=200ss"
  },
  {
    "path": "DXMainClient/Resources/INI/ai.ini",
    "content": "[TaskForces]\n0=0832C3F0-G\n1=07ECC200-G\n2=08063D20-G\n3=08063DE0-G\n4=0804C6C0-G\n5=0804C530-G\n6=073A9510-G\n7=073A9CC0-G\n8=07ECD1F0-G\n9=075BFDC0-G\n10=0859E2E0-G\n11=07EA4290-G\n12=073A8CF0-G\n13=07ECE4A0-G\n14=084B2F60-G\n15=07ECFB60-G\n16=096473E0-G\n17=08603140-G\n18=07EA1E90-G\n19=08602820-G\n20=0860DE90-G\n21=07ED0860-G\n22=07ED0460-G\n23=08050A00-G\n24=07ED1330-G\n25=073AACA0-G\n26=075E02F0-G\n27=09F7B380-G\n28=073AA3F0-G\n29=075E2310-G\n30=075E2180-G\n31=07E04DC0-G\n32=085EC2D0-G\n33=09EF0540-G\n34=09EF2BB0-G\n35=09F11CE0-G\n36=08600780-G\n37=0965B600-G\n38=086001F0-G\n39=0965D960-G\n40=08600DC0-G\n41=08601B60-G\n42=086019D0-G\n43=08601770-G\n44=086015E0-G\n45=08601380-G\n46=0846A4D0-G\n47=08602B40-G\n48=086029B0-G\n49=08602640-G\n50=097246D0-G\n51=08602DC0-G\n52=086039B0-G\n53=0805DAC0-G\n54=08060740-G\n55=07F3CAE0-G\n56=07F3A490-G\n57=0860B440-G\n\n[0832C3F0-G]\nName=1 amphibious APC, 5 engineers\n0=5,ENGINEER\n1=1,APC\nGroup=-1\n\n[07ECC200-G]\nName=1 subterranean APC, 5 engineers\n0=1,SAPC\n1=5,ENGINEER\nGroup=-1\n\n[08063D20-G]\nName=1 subterranean APC, 3 eng, 2 roc\n0=1,SAPC\n1=3,ENGINEER\n2=2,E3\nGroup=-1\n\n[08063DE0-G]\nName=1 amphibious APC,3 eng, 2 disc\n0=1,APC\n1=3,ENGINEER\n2=2,E2\nGroup=-1\n\n[0804C6C0-G]\nName=1 amphibious APC,1 ghost,4 disc\n0=1,GHOST\n1=1,APC\n2=4,E2\nGroup=-1\n\n[0804C530-G]\nName=1 sub. APC, 1 cyb com, 4 cyborgs\n0=4,CYBORG\n1=1,SAPC\n2=1,CYC2\nGroup=-1\n\n[073A9510-G]\nName=4 attack cycles\n0=4,BIKE\nGroup=-1\n\n[073A9CC0-G]\nName=4 stealth tanks\n0=4,STNK\nGroup=-1\n\n[07ECD1F0-G]\nName=3 wolverines\n0=3,SMECH\nGroup=-1\n\n[075BFDC0-G]\nName=3 hover MLRS\n0=3,HVR\nGroup=-1\n\n[0859E2E0-G]\nName=1 mobile repair vehicle\n0=1,REPAIR\nGroup=-1\n\n[07EA4290-G]\nName=3 artillery\n0=3,ART2\nGroup=-1\n\n[073A8CF0-G]\nName=4 tick tanks\n0=4,TTNK\nGroup=-1\n\n[07ECE4A0-G]\nName=4 devil's tongue\n0=4,SUBTANK\nGroup=-1\n\n[084B2F60-G]\nName=1 amphibious  APC\n0=1,APC\nGroup=-1\n\n[07ECFB60-G]\nName=3 titans\n0=3,MMCH\nGroup=-1\n\n[096473E0-G]\nName=3 disruptors\n0=3,SONIC\nGroup=-1\n\n[08603140-G]\nName=1 ORCA fighters\n0=1,ORCA\nGroup=-1\n\n[07EA1E90-G]\nName=1 ORCA bombers\n0=1,ORCAB\nGroup=-1\n\n[08602820-G]\nName=1 harpies\n0=1,APACHE\nGroup=-1\n\n[0860DE90-G]\nName=1 banshee\n0=1,SCRIN\nGroup=-1\n\n[07ED0860-G]\nName=4 titans\n0=4,MMCH\nGroup=-1\n\n[07ED0460-G]\nName=1 mammoth mk. II\n0=1,HMEC\nGroup=-1\n\n[08050A00-G]\nName=1 sub APC, 1 hijacker, 4 rocket\n0=1,MHIJACK\n1=1,SAPC\n2=4,E3\nGroup=-1\n\n[07ED1330-G]\nName=4 wolverines\n0=4,SMECH\nGroup=-1\n\n[073AACA0-G]\nName=4 attack buggies\n0=4,BGGY\nGroup=-1\n\n[075E02F0-G]\nName=4 disc throwers\n0=4,E2\nGroup=-1\n\n[09F7B380-G]\nName=3 jumpjet infantry\n0=3,JUMPJET\nGroup=-1\n\n[073AA3F0-G]\nName=1 mutant hijacker\n0=1,MHIJACK\nGroup=-1\n\n[075E2310-G]\nName=4 rocket infantry\n0=4,E3\nGroup=-1\n\n[075E2180-G]\nName=4 cyborgs\n0=4,CYBORG\nGroup=-1\n\n[07E04DC0-G]\nName=4 light infantry\n0=4,E1\nGroup=-1\n\n[085EC2D0-G]\nName=1 subterranean APC\n0=1,SAPC\nGroup=-1\n\n[09EF0540-G]\nName=3 engineers\n0=3,ENGINEER\nGroup=-1\n\n[09EF2BB0-G]\nName=1 ghoststalker\n0=1,GHOST\nGroup=-1\n\n[09F11CE0-G]\nName=1 cyborg commando\n0=1,CYC2\nGroup=-1\n\n[08600780-G]\nName=1 titans\n0=1,MMCH\nGroup=-1\n\n[0965B600-G]\nName=1 hover MLRS\n0=1,HVR\nGroup=-1\n\n[086001F0-G]\nName=1 disruptors\n0=1,SONIC\nGroup=-1\n\n[0965D960-G]\nName=1 disruptor\n0=1,SONIC\nGroup=-1\n\n[08600DC0-G]\nName=1 disc throwers\n0=1,E2\nGroup=-1\n\n[08601B60-G]\nName=1 jumpjet infantry\n0=1,JUMPJET\nGroup=-1\n\n[086019D0-G]\nName=1 tick tanks\n0=1,TTNK\nGroup=-1\n\n[08601770-G]\nName=1 stealth tanks\n0=1,STNK\nGroup=-1\n\n[086015E0-G]\nName=1 rocket infantry\n0=1,E3\nGroup=-1\n\n[08601380-G]\nName=1 cyborgs\n0=1,CYBORG\nGroup=-1\n\n[0846A4D0-G]\nName=1 attack cycles\n0=1,BIKE\nGroup=-1\n\n[08602B40-G]\nName=1 devil's tongue\n0=1,SUBTANK\nGroup=-1\n\n[086029B0-G]\nName=1 engineers\n0=1,ENGINEER\nGroup=-1\n\n[08602640-G]\nName=1 light infantry\n0=1,E1\nGroup=-1\n\n[097246D0-G]\nName=1 artillery\n0=1,ART2\nGroup=-1\n\n[08602DC0-G]\nName=1 attack buggies\n0=1,BGGY\nGroup=-1\n\n[086039B0-G]\nName=1 wolverines\n0=1,SMECH\nGroup=-1\n\n[0805DAC0-G]\nName=1 amph APC,1 eng, 2 lt. 2 disc\n0=1,ENGINEER\n1=1,APC\n2=2,E2\n3=2,E1\nGroup=-1\n\n[08060740-G]\nName=1 sub APC, 1 eng, 2 rocket, 2 lt\n0=1,ENGINEER\n1=1,SAPC\n2=2,E1\n3=2,E3\nGroup=-1\n\n[07F3CAE0-G]\nName=1 sub. APC, 3 lt, 2 cyborgs\n0=2,CYBORG\n1=3,E1\n2=1,SAPC\nGroup=-1\n\n[07F3A490-G]\nName=1 amphibious APC, 3 lt, 2 disc\n0=1,APC\n1=2,E2\n2=3,E1\nGroup=-1\n\n[0860B440-G]\nName=1 MCV\n0=1,MCV\nGroup=-1\n\n[ScriptTypes]\n0=085F3E00-G\n1=085F3980-G\n2=075A3070-G\n3=07F7B2A0-G\n4=07E686F0-G\n5=07F7C5E0-G\n6=0786DA60-G\n7=07F7D0D0-G\n8=07F7E3B0-G\n9=0960AAA0-G\n10=07F76BE0-G\n11=075AD760-G\n12=08462780-G\n13=075ABE00-G\n14=08463030-G\n15=088DDE00-G\n16=07397BE0-G\n17=07F3DE00-G\n18=08B50140-G\n\n[085F3E00-G]\nName=APC/engineer attack\n0=14,0\n1=43,0\n2=47,131084\n3=49,0\n4=8,2\n5=11,14\n\n[085F3980-G]\nName=APC/eng. steal money\n0=14,0\n1=43,0\n2=47,131073\n3=49,0\n4=8,2\n5=46,131073\n6=46,131074\n7=11,14\n\n[075A3070-G]\nName=APC/commando attack\n0=14,0\n1=47,131084\n2=49,0\n3=8,2\n4=0,2\n5=0,1\n\n[07F7B2A0-G]\nName=Harvester attack\n0=0,3\n1=0,1\n\n[07E686F0-G]\nName=Base defense\n0=11,10\n\n[07F7C5E0-G]\nName=Base defense attack\n0=0,7\n1=0,1\n\n[0786DA60-G]\nName=Deployed base defense\n0=9,0\n1=11,10\n\n[07F7D0D0-G]\nName=Aerial base attack\n0=0,2\n1=0,1\n\n[07F7E3B0-G]\nName=Vehicle attack\n0=0,5\n1=0,1\n\n[0960AAA0-G]\nName=APC/thief steal vehicles\n0=14,0\n1=47,1\n2=49,0\n3=8,2\n4=0,5\n\n[07F76BE0-G]\nName=Infantry attack\n0=0,4\n1=0,1\n\n[075AD760-G]\nName=Construction yard attack\n0=46,131084\n1=49,0\n2=0,1\n\n[08462780-G]\nName=Factories attack\n0=0,6\n1=0,1\n\n[075ABE00-G]\nName=Tiberium refinery attack\n0=46,131073\n1=49,0\n2=0,1\n\n[08463030-G]\nName=Power facilities attack\n0=0,9\n1=0,1\n\n[088DDE00-G]\nName=Missile silo attack\n0=46,131113\n1=49,0\n2=0,1\n\n[07397BE0-G]\nName=Upgrade center attack\n0=46,131076\n1=49,0\n2=0,1\n\n[07F3DE00-G]\nName=APC/infantry attack\n0=14,0\n1=43,0\n2=47,12\n3=49,0\n4=8,2\n5=0,1\n\n[08B50140-G]\nName=Replace MCV\n0=9,0\n\n[TeamTypes]\n0=0832C790-G\n1=07EB8E90-G\n2=080646D0-G\n3=080643E0-G\n4=084D4AE0-G\n5=084D4730-G\n6=0832D7F0-G\n7=0832D440-G\n8=07E7F400-G\n9=0832D120-G\n10=07E7F0E0-G\n11=07E89580-G\n12=0832E770-G\n13=07ECE560-G\n14=0832E3D0-G\n15=0832E300-G\n16=0832E230-G\n17=0832E160-G\n18=090E0AC0-G\n19=090E0930-G\n20=090E07A0-G\n21=090E0620-G\n22=084B2AA0-G\n23=084B2910-G\n24=0B7D4E70-G\n25=0B7D4A90-G\n26=0B7D48E0-G\n27=0B7D4730-G\n28=0B7D4580-G\n29=0B7D44A0-G\n30=0B7D67E0-G\n31=0B7D6700-G\n32=07ECD3B0-G\n33=0753BE90-G\n34=07EA0540-G\n35=073A8540-G\n36=084D7070-G\n37=07EA0150-G\n38=090E2C20-G\n39=07EA1E80-G\n40=073A8070-G\n41=073A9E50-G\n42=073A9D80-G\n43=073A9BF0-G\n44=0B7F7500-G\n45=0B7F7420-G\n46=084D8A40-G\n47=073A95D0-G\n48=073A97A0-G\n49=073A93F0-G\n50=073A9320-G\n51=073A83B0-G\n52=07EA2AC0-G\n53=07EA2930-G\n54=0965C310-G\n55=073AA970-G\n56=0753EC30-G\n57=07EA22F0-G\n58=084D9710-G\n59=0753E780-G\n60=0753E5F0-G\n61=073AA2F0-G\n62=073AA220-G\n63=073AA150-G\n64=0753E130-G\n65=0753E060-G\n66=074A32F0-G\n67=073AB9B0-G\n68=084DAA80-G\n69=090E45F0-G\n70=090E4520-G\n71=090E4F50-G\n72=084DA6A0-G\n73=0B7F17D0-G\n74=084DA2E0-G\n75=084DA210-G\n76=084DA140-G\n77=084DA070-G\n78=0B7F6CA0-G\n79=0B7F6BC0-G\n80=084DBDB0-G\n81=0B7F6A00-G\n82=084DBB40-G\n83=0B7F6840-G\n84=084DB9A0-G\n85=084DB8D0-G\n86=084DB800-G\n87=084DB730-G\n88=073AC800-G\n89=0B7F4B50-G\n90=073AC440-G\n91=073ACE80-G\n92=073ACDB0-G\n93=073AC1B0-G\n94=0B7F46F0-G\n95=0B7F4610-G\n96=09649240-G\n97=0B7F4450-G\n98=0964EF50-G\n99=0B7F8070-G\n100=0964EBC0-G\n101=0964EAF0-G\n102=0964EA20-G\n103=0964E950-G\n104=07EF9730-G\n105=04167990-G\n106=07EA6C60-G\n107=07EA6B90-G\n108=07EF91D0-G\n109=04174D70-G\n110=075A55C0-G\n111=04174BB0-G\n112=07E89DD0-G\n113=04176E60-G\n114=073AEA00-G\n115=04173E00-G\n116=07E8F140-G\n117=07E8CF50-G\n118=073AE6C0-G\n119=07E8CDB0-G\n120=073AE790-G\n121=041742A0-G\n122=073AEE80-G\n123=073AE260-G\n124=073AE190-G\n125=073AE0C0-G\n126=041768B0-G\n127=041767D0-G\n128=0A9BBF50-G\n129=04181EE0-G\n130=0A9BBDB0-G\n131=04181D20-G\n132=0A9BBC10-G\n133=0A9BBB40-G\n134=0A9BBA70-G\n135=0A9BB9A0-G\n136=073AF400-G\n137=073AF270-G\n138=084DE340-G\n139=080F71A0-G\n140=084DFF50-G\n141=084DEA70-G\n142=084DE9A0-G\n143=084DFBF0-G\n144=080CCD80-G\n145=080CCCA0-G\n146=084DF980-G\n147=080CCAE0-G\n148=084DF7E0-G\n149=080CC920-G\n150=084DF640-G\n151=084DFE80-G\n152=084DFDB0-G\n153=084DFCE0-G\n154=084DF2C0-G\n155=084DF1F0-G\n156=084DF120-G\n157=084DF050-G\n158=084D7830-G\n159=084E0F50-G\n160=080CEA80-G\n161=084E08B0-G\n162=084E07E0-G\n163=084E0710-G\n164=084E0640-G\n165=080CE620-G\n166=080CE540-G\n167=084E03D0-G\n168=080CE380-G\n169=084E0230-G\n170=080CE1C0-G\n171=084E0090-G\n172=084E0B90-G\n173=084E0AC0-G\n174=084E09F0-G\n175=080CF910-G\n176=080CF780-G\n177=080CF6B0-G\n178=080CF520-G\n179=084E1790-G\n180=080CF380-G\n181=080CFC20-G\n182=080C70C0-G\n183=080D0D90-G\n184=08468050-G\n185=07D73AA0-G\n186=07D739D0-G\n187=07D73840-G\n188=07E4EA50-G\n189=07E4CF50-G\n190=086012B0-G\n191=084E1530-G\n192=084E1460-G\n193=097235F0-G\n194=084E21C0-G\n195=090EDA60-G\n196=090ED990-G\n197=084E3E80-G\n198=084E3CF0-G\n199=090ECA90-G\n200=084E3980-G\n201=090ED230-G\n202=08602F50-G\n203=090EDE80-G\n204=090EDCF0-G\n205=090EDC20-G\n206=084E32D0-G\n207=090EEB00-G\n208=084E3130-G\n209=084E3060-G\n210=084E4F50-G\n211=084E38B0-G\n212=084E37E0-G\n213=090EEDB0-G\n214=090EECE0-G\n215=084E4AE0-G\n216=084E4A10-G\n217=090EE280-G\n218=090EE1B0-G\n219=084E47A0-G\n220=084E4610-G\n221=090EE700-G\n222=090EE630-G\n223=07F2A9F0-G\n224=07F2A920-G\n225=07F2A850-G\n226=084E5E80-G\n227=07EF7DF0-G\n228=07EF7D20-G\n229=084E5B50-G\n230=084E5A80-G\n231=07EFA790-G\n232=07EF88B0-G\n233=084E43A0-G\n234=084E42D0-G\n235=084E55C0-G\n236=084E54F0-G\n237=084E5420-G\n238=084E5350-G\n239=084E5280-G\n240=084E51B0-G\n241=090F0340-G\n242=080D32A0-G\n243=080D31D0-G\n244=080D3100-G\n245=080D3030-G\n246=080D4A70-G\n247=080D49A0-G\n248=080D48D0-G\n249=080D4800-G\n250=07E30830-G\n251=07E30760-G\n252=080D4590-G\n253=080D44C0-G\n254=080D43F0-G\n255=080D4320-G\n256=080D4250-G\n257=080D4180-G\n258=080D40B0-G\n259=084E64C0-G\n260=084E63F0-G\n261=084E6320-G\n262=084E6250-G\n263=084E6180-G\n264=084E60B0-G\n265=084E7F50-G\n266=084E7E80-G\n267=080D58D0-G\n268=080D5800-G\n269=080D5730-G\n270=080D5660-G\n271=080D5590-G\n272=080D54C0-G\n273=080D53F0-G\n274=080D5320-G\n275=07ECC9F0-G\n276=07ECC920-G\n277=07ECC750-G\n278=07ECC680-G\n279=07ECC4B0-G\n280=07ECC3E0-G\n281=07ECC210-G\n282=07ECC140-G\n283=084E70B0-G\n284=084D9250-G\n285=084DF570-G\n286=084DF4A0-G\n287=084DF3D0-G\n288=084E8F50-G\n289=084E8E80-G\n290=084E8DB0-G\n291=09D00530-G\n292=08607590-G\n293=086074C0-G\n294=084E8A70-G\n295=07E8D320-G\n296=07E8D250-G\n297=084E8740-G\n298=084E8670-G\n299=084E85A0-G\n300=084E84D0-G\n301=084E8400-G\n302=084E8330-G\n303=084E8260-G\n304=084E8190-G\n305=084E80C0-G\n306=084E9F50-G\n307=084E9E80-G\n308=080D7590-G\n309=080D74C0-G\n310=080D73F0-G\n311=080D7320-G\n312=080D7250-G\n313=080D7180-G\n314=080D70B0-G\n315=080C8F50-G\n316=086083F0-G\n317=09D6EF50-G\n318=080CF110-G\n319=07FCFC60-G\n320=07FCFB90-G\n321=07FCFAC0-G\n322=08750120-G\n323=080D8CE0-G\n324=084E90B0-G\n325=084EAF50-G\n326=084EAE80-G\n327=0988C920-G\n328=084EACE0-G\n329=084EAC10-G\n330=084EAB40-G\n331=084EAA70-G\n332=09798770-G\n333=086094D0-G\n334=08609400-G\n335=09798300-G\n336=09798130-G\n337=09798060-G\n338=09799E50-G\n339=09799D80-G\n340=09E58F50-G\n341=0860ADB0-G\n342=0860ACE0-G\n343=07FC54B0-G\n344=07FC52E0-G\n345=07FC5210-G\n346=07FC5040-G\n347=07FC4F50-G\n348=084EBC10-G\n349=084EBB40-G\n350=084EBA70-G\n351=084EB9A0-G\n352=084EB8D0-G\n353=084EB800-G\n354=084EB730-G\n355=084EB660-G\n356=09EF4700-G\n357=09EF4630-G\n358=07F3C110-G\n359=07F3DAD0-G\n360=07F3DA00-G\n361=07F3D930-G\n362=084ECBA0-G\n363=084EC7F0-G\n364=084EC720-G\n365=084EC650-G\n366=080DA100-G\n367=0AA4F200-G\n368=080DBF50-G\n369=080DBE80-G\n370=08BBE7F0-G\n371=08BBE720-G\n372=080DBC10-G\n373=080DBB40-G\n374=08BBC6B0-G\n375=08BBC5E0-G\n376=080DB8D0-G\n377=080DB800-G\n378=0AA34AD0-G\n379=0AA34A00-G\n380=080DB590-G\n381=080DB4C0-G\n382=0AA34190-G\n383=0AA340C0-G\n384=0AA35B50-G\n385=0AA35A80-G\n386=0AA36F50-G\n387=0AA36E80-G\n\n[0832C790-G]\nName=H_GDI APC/engineer attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3E00-G\nTaskForce=0832C3F0-G\n\n[07EB8E90-G]\nName=H_Nod APC/engineers attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3E00-G\nTaskForce=07ECC200-G\n\n[080646D0-G]\nName=H_Nod APC/eng. steal money\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3980-G\nTaskForce=08063D20-G\n\n[080643E0-G]\nName=H_GDI APC/eng. steal money\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3980-G\nTaskForce=08063DE0-G\n\n[084D4AE0-G]\nName=H_GDI APC/commando attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075A3070-G\nTaskForce=0804C6C0-G\n\n[084D4730-G]\nName=H_Nod APC/commando attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075A3070-G\nTaskForce=0804C530-G\n\n[0832D7F0-G]\nName=H_Nod harvester attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=073A9510-G\n\n[0832D440-G]\nName=H_Nod harvester attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=073A9CC0-G\n\n[07E7F400-G]\nName=H_GDI harvester attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=07ECD1F0-G\n\n[0832D120-G]\nName=H_GDI harvester attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=075BFDC0-G\n\n[07E7F0E0-G]\nName=H_Nod base repair vehicle\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0859E2E0-G\n\n[07E89580-G]\nName=H_Nod base defense attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=07EA4290-G\n\n[0832E770-G]\nName=H_Nod base defense attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=073A8CF0-G\n\n[07ECE560-G]\nName=H_Nod devil's tongue pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07ECE4A0-G\n\n[0832E3D0-G]\nName=H_Nod tank pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=073A8CF0-G\n\n[0832E300-G]\nName=H_Nod ranged pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=07EA4290-G\n\n[0832E230-G]\nName=H_Nod recon pool 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073A9510-G\n\n[0832E160-G]\nName=H_Nod stealth pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073A9CC0-G\n\n[090E0AC0-G]\nName=H_GDI amphibious pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=084B2F60-G\n\n[090E0930-G]\nName=H_GDI titan pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07ECFB60-G\n\n[090E07A0-G]\nName=H_GDI wolverine pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07ECD1F0-G\n\n[090E0620-G]\nName=H_GDI hover pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075BFDC0-G\n\n[084B2AA0-G]\nName=H_GDI base defense attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=075BFDC0-G\n\n[084B2910-G]\nName=H_GDI base defense attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=096473E0-G\n\n[0B7D4E70-G]\nName=H_GDI aerial base attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=08603140-G\n\n[0B7D4A90-G]\nName=H_GDI aerial base attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=07EA1E90-G\n\n[0B7D48E0-G]\nName=H_Nod aerial base attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=08602820-G\n\n[0B7D4730-G]\nName=H_Nod aerial base attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=0860DE90-G\n\n[0B7D4580-G]\nName=H_GDI ORCA fighter pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08603140-G\n\n[0B7D44A0-G]\nName=H_GDI ORCA bomber pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07EA1E90-G\n\n[0B7D67E0-G]\nName=H_Nod banshee pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0860DE90-G\n\n[0B7D6700-G]\nName=H_Nod harpy pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602820-G\n\n[07ECD3B0-G]\nName=H_GDI vehicle attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07ED0860-G\n\n[0753BE90-G]\nName=H_GDI vehicle attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07ECFB60-G\n\n[07EA0540-G]\nName=H_GDI vehicle attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075BFDC0-G\n\n[073A8540-G]\nName=H_GDI vehicle attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075BFDC0-G\n\n[084D7070-G]\nName=H_GDI vehicle attack 4\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07ED0460-G\n\n[07EA0150-G]\nName=H_GDI vehicle attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=096473E0-G\n\n[090E2C20-G]\nName=H_GDI disrupter pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=096473E0-G\n\n[07EA1E80-G]\nName=H_GDI vehicle attack 6a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=096473E0-G\n\n[073A8070-G]\nName=H_Nod vehicle attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073A8CF0-G\n\n[073A9E50-G]\nName=H_Nod vehicle attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073A8CF0-G\n\n[073A9D80-G]\nName=H_Nod vehicle attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073A9CC0-G\n\n[073A9BF0-G]\nName=H_Nod vehicle attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073A9CC0-G\n\n[0B7F7500-G]\nName=H_Nod aerial vehicle attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0860DE90-G\n\n[0B7F7420-G]\nName=H_GDI aerial vehicle attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08603140-G\n\n[084D8A40-G]\nName=H_Nod APC/thief steal vehicles\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=0960AAA0-G\nTaskForce=08050A00-G\n\n[073A95D0-G]\nName=H_Nod vehicle attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073A9510-G\n\n[073A97A0-G]\nName=H_GDI infantry attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07ED1330-G\n\n[073A93F0-G]\nName=H_GDI infantry attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07ECD1F0-G\n\n[073A9320-G]\nName=H_GDI infantry attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=096473E0-G\n\n[073A83B0-G]\nName=H_GDI infantry attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=096473E0-G\n\n[07EA2AC0-G]\nName=H_Nod infantry attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=073AACA0-G\n\n[07EA2930-G]\nName=H_Nod infantry attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=073AACA0-G\n\n[0965C310-G]\nName=H_Nod infantry attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07ECE4A0-G\n\n[073AA970-G]\nName=H_Nod infantry attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07ECE4A0-G\n\n[0753EC30-G]\nName=H_GDI vehicle attack 6b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075E02F0-G\n\n[07EA22F0-G]\nName=H_GDI vehicle attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=09F7B380-G\n\n[084D9710-G]\nName=H_Nod vehicle attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073AA3F0-G\n\n[0753E780-G]\nName=H_Nod vehicle attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075E2310-G\n\n[0753E5F0-G]\nName=H_Nod vehicle attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075E2180-G\n\n[073AA2F0-G]\nName=H_GDI infantry attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=09F7B380-G\n\n[073AA220-G]\nName=H_GDI infantry attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=075E02F0-G\n\n[073AA150-G]\nName=H_GDI infantry attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07E04DC0-G\n\n[0753E130-G]\nName=H_Nod infantry attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07E04DC0-G\n\n[0753E060-G]\nName=H_Nod infantry attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=075E2180-G\n\n[074A32F0-G]\nName=H_Nod rocket infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075E2310-G\n\n[073AB9B0-G]\nName=H_Nod light infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07E04DC0-G\n\n[084DAA80-G]\nName=H_Nod cyborg pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075E2180-G\n\n[090E45F0-G]\nName=H_GDI light infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07E04DC0-G\n\n[090E4520-G]\nName=H_GDI jumpjet infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09F7B380-G\n\n[090E4F50-G]\nName=H_GDI disc thrower pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075E02F0-G\n\n[084DA6A0-G]\nName=H_GDI con. yard attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07ED0860-G\n\n[0B7F17D0-G]\nName=H_GDI con. yard attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07EA1E90-G\n\n[084DA2E0-G]\nName=H_GDI con. yard attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=075BFDC0-G\n\n[084DA210-G]\nName=H_GDI con. yard attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=09F7B380-G\n\n[084DA140-G]\nName=H_GDI con. yard attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=096473E0-G\n\n[084DA070-G]\nName=H_GDI con. yard attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=075E02F0-G\n\n[0B7F6CA0-G]\nName=H_GDI con. yard attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08603140-G\n\n[0B7F6BC0-G]\nName=H_GDI con. yard attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07EA1E90-G\n\n[084DBDB0-G]\nName=H_Nod con. yard attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=073A8CF0-G\n\n[0B7F6A00-G]\nName=H_Nod con. yard attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0860DE90-G\n\n[084DBB40-G]\nName=H_Nod con. yard attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=073A9510-G\n\n[0B7F6840-G]\nName=H_Nod con. yard attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08602820-G\n\n[084DB9A0-G]\nName=H_Nod con. yard attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07ECE4A0-G\n\n[084DB8D0-G]\nName=H_Nod con. yard attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07EA4290-G\n\n[084DB800-G]\nName=H_Nod con. yard attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=073A9CC0-G\n\n[084DB730-G]\nName=H_Nod con. yard attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=075E2310-G\n\n[073AC800-G]\nName=H_GDI factories attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07ED0860-G\n\n[0B7F4B50-G]\nName=H_GDI factories attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07EA1E90-G\n\n[073AC440-G]\nName=H_GDI factories attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=075BFDC0-G\n\n[073ACE80-G]\nName=H_GDI factories attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=09F7B380-G\n\n[073ACDB0-G]\nName=H_GDI factories attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=096473E0-G\n\n[073AC1B0-G]\nName=H_GDI factories attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=075E02F0-G\n\n[0B7F46F0-G]\nName=H_GDI factories attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08603140-G\n\n[0B7F4610-G]\nName=H_GDI factories attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07EA1E90-G\n\n[09649240-G]\nName=H_Nod factories attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=073A8CF0-G\n\n[0B7F4450-G]\nName=H_Nod factories attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0860DE90-G\n\n[0964EF50-G]\nName=H_Nod factories attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=073A9510-G\n\n[0B7F8070-G]\nName=H_Nod factories attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08602820-G\n\n[0964EBC0-G]\nName=H_Nod factories attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07ECE4A0-G\n\n[0964EAF0-G]\nName=H_Nod factories attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07EA4290-G\n\n[0964EA20-G]\nName=H_Nod factories attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=073A9CC0-G\n\n[0964E950-G]\nName=H_Nod factories attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=075E2310-G\n\n[07EF9730-G]\nName=H_GDI tib. refinery attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07ED0860-G\n\n[04167990-G]\nName=H_GDI tib. refinery attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07EA1E90-G\n\n[07EA6C60-G]\nName=H_GDI tib. refinery attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=075BFDC0-G\n\n[07EA6B90-G]\nName=H_GDI tib. refinery attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=09F7B380-G\n\n[07EF91D0-G]\nName=H_GDI tib. refinery attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=096473E0-G\n\n[04174D70-G]\nName=H_GDI tib. refinery attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08603140-G\n\n[075A55C0-G]\nName=H_GDI tib. refinery attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=075E02F0-G\n\n[04174BB0-G]\nName=H_GDI tib. refinery attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07EA1E90-G\n\n[07E89DD0-G]\nName=H_Nod tib. refinery attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=073A8CF0-G\n\n[04176E60-G]\nName=H_Nod tib. refinery attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0860DE90-G\n\n[073AEA00-G]\nName=H_Nod tib. refinery attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=073A9510-G\n\n[04173E00-G]\nName=H_Nod tib. refinery attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08602820-G\n\n[07E8F140-G]\nName=H_Nod tib. refinery attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07ECE4A0-G\n\n[07E8CF50-G]\nName=H_Nod tib. refinery attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07EA4290-G\n\n[073AE6C0-G]\nName=H_Nod tib. refinery attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=073A9CC0-G\n\n[07E8CDB0-G]\nName=H_Nod tib. refinery attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=075E2310-G\n\n[073AE790-G]\nName=H_GDI power facility attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07ED0860-G\n\n[041742A0-G]\nName=H_GDI power facility attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07EA1E90-G\n\n[073AEE80-G]\nName=H_GDI power facility attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=075BFDC0-G\n\n[073AE260-G]\nName=H_GDI power facility attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=09F7B380-G\n\n[073AE190-G]\nName=H_GDI power facility attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=096473E0-G\n\n[073AE0C0-G]\nName=H_GDI power facility attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=075E02F0-G\n\n[041768B0-G]\nName=H_GDI power facility attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08603140-G\n\n[041767D0-G]\nName=H_GDI power facility attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07EA1E90-G\n\n[0A9BBF50-G]\nName=H_Nod power facility attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=073A8CF0-G\n\n[04181EE0-G]\nName=H_Nod power facility attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0860DE90-G\n\n[0A9BBDB0-G]\nName=H_Nod power facility attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=073A9510-G\n\n[04181D20-G]\nName=H_Nod power facility attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08602820-G\n\n[0A9BBC10-G]\nName=H_Nod power facility attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07ECE4A0-G\n\n[0A9BBB40-G]\nName=H_Nod power facility attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07EA4290-G\n\n[0A9BBA70-G]\nName=H_Nod power facility attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=073A9CC0-G\n\n[0A9BB9A0-G]\nName=H_Nod power facility attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=075E2310-G\n\n[073AF400-G]\nName=H_Nod subterranean  APC pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=085EC2D0-G\n\n[073AF270-G]\nName=H_Nod recon pool 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073AACA0-G\n\n[084DE340-G]\nName=H_GDI missile silo attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07ED0860-G\n\n[080F71A0-G]\nName=H_GDI missile silo attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07EA1E90-G\n\n[084DFF50-G]\nName=H_GDI missile silo attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=075BFDC0-G\n\n[084DEA70-G]\nName=H_GDI missile silo attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=09F7B380-G\n\n[084DE9A0-G]\nName=H_GDI missile silo attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=096473E0-G\n\n[084DFBF0-G]\nName=H_GDI missile silo attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=075E02F0-G\n\n[080CCD80-G]\nName=H_GDI missile silo attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08603140-G\n\n[080CCCA0-G]\nName=H_GDI missile silo attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07EA1E90-G\n\n[084DF980-G]\nName=H_Nod missile silo attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=073A8CF0-G\n\n[080CCAE0-G]\nName=H_Nod missile silo attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0860DE90-G\n\n[084DF7E0-G]\nName=H_Nod missile silo attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=073A9510-G\n\n[080CC920-G]\nName=H_Nod missile silo attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08602820-G\n\n[084DF640-G]\nName=H_Nod missile silo attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07ECE4A0-G\n\n[084DFE80-G]\nName=H_Nod missile silo attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07EA4290-G\n\n[084DFDB0-G]\nName=H_Nod missile silo attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=073A9CC0-G\n\n[084DFCE0-G]\nName=H_Nod missile silo attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=075E2310-G\n\n[084DF2C0-G]\nName=H_Nod engineer pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09EF0540-G\n\n[084DF1F0-G]\nName=H_GDI engineer pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09EF0540-G\n\n[084DF120-G]\nName=H_Nod mutant hijacker pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073AA3F0-G\n\n[084DF050-G]\nName=H_GDI ghoststalker pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09EF2BB0-G\n\n[084D7830-G]\nName=H_Nod cyborg commando pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09F11CE0-G\n\n[084E0F50-G]\nName=H_GDI upgrade center attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07ED0860-G\n\n[080CEA80-G]\nName=H_GDI upgrade center attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07EA1E90-G\n\n[084E08B0-G]\nName=H_GDI upgrade center attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=075BFDC0-G\n\n[084E07E0-G]\nName=H_GDI upgrade center attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=09F7B380-G\n\n[084E0710-G]\nName=H_GDI upgrade center attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=096473E0-G\n\n[084E0640-G]\nName=H_GDI upgrade center attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=075E02F0-G\n\n[080CE620-G]\nName=H_GDI upgrade center attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08603140-G\n\n[080CE540-G]\nName=H_GDI upgrade center attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07EA1E90-G\n\n[084E03D0-G]\nName=H_Nod upgrade center attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=073A8CF0-G\n\n[080CE380-G]\nName=H_Nod upgrade center attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0860DE90-G\n\n[084E0230-G]\nName=H_Nod upgrade center attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=073A9510-G\n\n[080CE1C0-G]\nName=H_Nod upgrade center attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08602820-G\n\n[084E0090-G]\nName=H_Nod upgrade center attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07ECE4A0-G\n\n[084E0B90-G]\nName=H_Nod upgrade center attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07EA4290-G\n\n[084E0AC0-G]\nName=H_Nod upgrade center attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=073A9CC0-G\n\n[084E09F0-G]\nName=H_Nod upgrade center attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=075E2310-G\n\n[080CF910-G]\nName=E_GDI vehicle attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08600780-G\n\n[080CF780-G]\nName=E_GDI vehicle attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08600780-G\n\n[080CF6B0-G]\nName=E_GDI vehicle attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0965B600-G\n\n[080CF520-G]\nName=E_GDI vehicle attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0965B600-G\n\n[084E1790-G]\nName=E_GDI vehicle attack 4\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07ED0460-G\n\n[080CF380-G]\nName=E_GDI vehicle attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=086001F0-G\n\n[080CFC20-G]\nName=E_GDI vehicle attack 6a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0965D960-G\n\n[080C70C0-G]\nName=E_GDI vehicle attack 6b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08600DC0-G\n\n[080D0D90-G]\nName=E_GDI vehicle attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08601B60-G\n\n[08468050-G]\nName=E_Nod vehicle attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=086019D0-G\n\n[07D73AA0-G]\nName=E_Nod vehicle attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=086019D0-G\n\n[07D739D0-G]\nName=E_Nod vehicle attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08601770-G\n\n[07D73840-G]\nName=E_Nod vehicle attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=086015E0-G\n\n[07E4EA50-G]\nName=E_Nod vehicle attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08601770-G\n\n[07E4CF50-G]\nName=E_Nod vehicle attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08601380-G\n\n[086012B0-G]\nName=E_Nod vehicle attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0846A4D0-G\n\n[084E1530-G]\nName=E_Nod vehicle attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=073AA3F0-G\n\n[084E1460-G]\nName=E_Nod banshee pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0860DE90-G\n\n[097235F0-G]\nName=E_Nod base repair vehicle pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0859E2E0-G\n\n[084E21C0-G]\nName=E_Nod cyborg commando pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09F11CE0-G\n\n[090EDA60-G]\nName=E_Nod cyborg pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08601380-G\n\n[090ED990-G]\nName=E_Nod devil's tongue pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602B40-G\n\n[084E3E80-G]\nName=E_Nod engineer pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086029B0-G\n\n[084E3CF0-G]\nName=E_Nod harpy pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602820-G\n\n[090ECA90-G]\nName=E_Nod light infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602640-G\n\n[084E3980-G]\nName=E_Nod mutant hijacker pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073AA3F0-G\n\n[090ED230-G]\nName=E_Nod ranged pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=097246D0-G\n\n[08602F50-G]\nName=E_Nod recon pool 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0846A4D0-G\n\n[090EDE80-G]\nName=E_Nod recon pool 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602DC0-G\n\n[090EDCF0-G]\nName=E_Nod rocket infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086015E0-G\n\n[090EDC20-G]\nName=E_Nod stealth pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08601770-G\n\n[084E32D0-G]\nName=E_Nod subterranean APC pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=085EC2D0-G\n\n[090EEB00-G]\nName=E_Nod tank pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=086019D0-G\n\n[084E3130-G]\nName=H_Nod base air defense 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075E2310-G\n\n[084E3060-G]\nName=H_Nod base air defense 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=073A9CC0-G\n\n[084E4F50-G]\nName=H_GDI base air defense 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09F7B380-G\n\n[084E38B0-G]\nName=H_GDI base air defense 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=075BFDC0-G\n\n[084E37E0-G]\nName=E_GDI amphibious pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=084B2F60-G\n\n[090EEDB0-G]\nName=E_GDI disc thrower pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08600DC0-G\n\n[090EECE0-G]\nName=E_GDI disruptor pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086001F0-G\n\n[084E4AE0-G]\nName=E_GDI engineer pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086029B0-G\n\n[084E4A10-G]\nName=E_GDI ghostalker pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=09EF2BB0-G\n\n[090EE280-G]\nName=E_GDI hover pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0965B600-G\n\n[090EE1B0-G]\nName=E_GDI jumpjet infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08601B60-G\n\n[084E47A0-G]\nName=E_GDI ORCA bomber pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=07EA1E90-G\n\n[084E4610-G]\nName=E_GDI ORCA fighter pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08603140-G\n\n[090EE700-G]\nName=E_GDI titan pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08600780-G\n\n[090EE630-G]\nName=E_GDI wolverine pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086039B0-G\n\n[07F2A9F0-G]\nName=E_GDI aerial base attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=08603140-G\n\n[07F2A920-G]\nName=E_GDI aerial base attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=07EA1E90-G\n\n[07F2A850-G]\nName=E_GDI aerial vehicle attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08603140-G\n\n[084E5E80-G]\nName=E_GDI APC/commando attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075A3070-G\nTaskForce=0804C6C0-G\n\n[07EF7DF0-G]\nName=E_GDI APC/eng. steal money\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3980-G\nTaskForce=0805DAC0-G\n\n[07EF7D20-G]\nName=E_GDI APC/engineer attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3E00-G\nTaskForce=0805DAC0-G\n\n[084E5B50-G]\nName=E_GDI base air defense 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08601B60-G\n\n[084E5A80-G]\nName=E_GDI base air defense 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0965B600-G\n\n[07EFA790-G]\nName=E_GDI base defense attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=0965B600-G\n\n[07EF88B0-G]\nName=E_GDI base defense attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=0965D960-G\n\n[084E43A0-G]\nName=E_GDI con. yard attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08600780-G\n\n[084E42D0-G]\nName=E_GDI con. yard attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07EA1E90-G\n\n[084E55C0-G]\nName=E_GDI con. yard attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08601B60-G\n\n[084E54F0-G]\nName=E_GDI con. yard attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0965D960-G\n\n[084E5420-G]\nName=E_GDI con. yard attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0965D960-G\n\n[084E5350-G]\nName=E_GDI con. yard attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08600DC0-G\n\n[084E5280-G]\nName=E_GDI con. yard attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08603140-G\n\n[084E51B0-G]\nName=E_GDI con. yard attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=07EA1E90-G\n\n[090F0340-G]\nName=E_GDI light infantry pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08602640-G\n\n[080D32A0-G]\nName=E_GDI factories attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08600780-G\n\n[080D31D0-G]\nName=E_GDI factories attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07EA1E90-G\n\n[080D3100-G]\nName=E_GDI factories attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08601B60-G\n\n[080D3030-G]\nName=E_GDI factories attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0965D960-G\n\n[080D4A70-G]\nName=E_GDI factories attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0965D960-G\n\n[080D49A0-G]\nName=E_GDI factories attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08600DC0-G\n\n[080D48D0-G]\nName=E_GDI factories attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08603140-G\n\n[080D4800-G]\nName=E_GDI factories attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07EA1E90-G\n\n[07E30830-G]\nName=E_GDI harvester attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=086039B0-G\n\n[07E30760-G]\nName=E_GDI harvester attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=0965B600-G\n\n[080D4590-G]\nName=E_GDI infantry attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=086039B0-G\n\n[080D44C0-G]\nName=E_GDI infantry attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=086039B0-G\n\n[080D43F0-G]\nName=E_GDI infantry attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08601B60-G\n\n[080D4320-G]\nName=E_GDI infantry attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=0965D960-G\n\n[080D4250-G]\nName=E_GDI infantry attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=0965D960-G\n\n[080D4180-G]\nName=E_GDI infantry attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08600DC0-G\n\n[080D40B0-G]\nName=E_GDI infantry attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602640-G\n\n[084E64C0-G]\nName=E_GDI missile silo attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08600780-G\n\n[084E63F0-G]\nName=E_GDI missile silo attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07EA1E90-G\n\n[084E6320-G]\nName=E_GDI missile silo attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08601B60-G\n\n[084E6250-G]\nName=E_GDI missile silo attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0965D960-G\n\n[084E6180-G]\nName=E_GDI missile silo attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0965D960-G\n\n[084E60B0-G]\nName=E_GDI missile silo attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08600DC0-G\n\n[084E7F50-G]\nName=E_GDI missile silo attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08603140-G\n\n[084E7E80-G]\nName=E_GDI missile silo attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=07EA1E90-G\n\n[080D58D0-G]\nName=E_GDI power facility attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08600780-G\n\n[080D5800-G]\nName=E_GDI power facility attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07EA1E90-G\n\n[080D5730-G]\nName=E_GDI power facility attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08601B60-G\n\n[080D5660-G]\nName=E_GDI power facility attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0965D960-G\n\n[080D5590-G]\nName=E_GDI power facility attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0965D960-G\n\n[080D54C0-G]\nName=E_GDI power facility attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08600DC0-G\n\n[080D53F0-G]\nName=E_GDI power facility attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08603140-G\n\n[080D5320-G]\nName=E_GDI power facility attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=07EA1E90-G\n\n[07ECC9F0-G]\nName=E_GDI tib. refinery attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08600780-G\n\n[07ECC920-G]\nName=E_GDI tib. refinery attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07EA1E90-G\n\n[07ECC750-G]\nName=E_GDI tib. refinery attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08601B60-G\n\n[07ECC680-G]\nName=E_GDI tib. refinery attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0965D960-G\n\n[07ECC4B0-G]\nName=E_GDI tib. refinery attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0965D960-G\n\n[07ECC3E0-G]\nName=E_GDI tib. refinery attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08600DC0-G\n\n[07ECC210-G]\nName=E_GDI tib. refinery attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08603140-G\n\n[07ECC140-G]\nName=E_GDI tib.refinery attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=07EA1E90-G\n\n[084E70B0-G]\nName=E_GDI upgrade center attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08600780-G\n\n[084D9250-G]\nName=E_GDI upgrade center attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07EA1E90-G\n\n[084DF570-G]\nName=E_GDI upgrade center attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08601B60-G\n\n[084DF4A0-G]\nName=E_GDI upgrade center attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0965D960-G\n\n[084DF3D0-G]\nName=E_GDI upgrade center attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0965D960-G\n\n[084E8F50-G]\nName=E_GDI upgrade center attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08600DC0-G\n\n[084E8E80-G]\nName=E_GDI upgrade center attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08603140-G\n\n[084E8DB0-G]\nName=E_GDI upgrade center attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=07EA1E90-G\n\n[09D00530-G]\nName=E_Nod aerial base attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=08602820-G\n\n[08607590-G]\nName=E_Nod aerial base attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7D0D0-G\nTaskForce=0860DE90-G\n\n[086074C0-G]\nName=E_Nod aerial vehicle attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=0860DE90-G\n\n[084E8A70-G]\nName=E_Nod APC/commando attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075A3070-G\nTaskForce=0804C530-G\n\n[07E8D320-G]\nName=E_Nod APC/eng. steal money\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3980-G\nTaskForce=08060740-G\n\n[07E8D250-G]\nName=E_Nod APC/engineer attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=085F3E00-G\nTaskForce=08060740-G\n\n[084E8740-G]\nName=E_Nod APC/thief steal vehicles\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=0960AAA0-G\nTaskForce=08050A00-G\n\n[084E8670-G]\nName=E_Nod base air defense 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=086015E0-G\n\n[084E85A0-G]\nName=E_Nod base air defense 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=3\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=08601770-G\n\n[084E84D0-G]\nName=E_Nod con. yard attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=086019D0-G\n\n[084E8400-G]\nName=E_Nod con. yard attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0860DE90-G\n\n[084E8330-G]\nName=E_Nod con. yard attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0846A4D0-G\n\n[084E8260-G]\nName=E_Nod con. yard attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08602820-G\n\n[084E8190-G]\nName=E_Nod con. yard attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08602B40-G\n\n[084E80C0-G]\nName=E_Nod con. yard attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=097246D0-G\n\n[084E9F50-G]\nName=E_Nod con. yard attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08601770-G\n\n[084E9E80-G]\nName=E_Nod con. yard attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=086015E0-G\n\n[080D7590-G]\nName=E_Nod factories attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=086019D0-G\n\n[080D74C0-G]\nName=E_Nod factories attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0860DE90-G\n\n[080D73F0-G]\nName=E_Nod factories attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0846A4D0-G\n\n[080D7320-G]\nName=E_Nod factories attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08602820-G\n\n[080D7250-G]\nName=E_Nod factories attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08602B40-G\n\n[080D7180-G]\nName=E_Nod factories attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=097246D0-G\n\n[080D70B0-G]\nName=E_Nod factories attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08601770-G\n\n[080C8F50-G]\nName=E_Nod factories attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=086015E0-G\n\n[086083F0-G]\nName=E_Nod harvester attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=0846A4D0-G\n\n[09D6EF50-G]\nName=E_Nod harvester attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7B2A0-G\nTaskForce=08601770-G\n\n[080CF110-G]\nName=E_Nod infantry attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602DC0-G\n\n[07FCFC60-G]\nName=E_Nod infantry attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602DC0-G\n\n[07FCFB90-G]\nName=E_Nod infantry attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08601380-G\n\n[07FCFAC0-G]\nName=E_Nod infantry attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602B40-G\n\n[08750120-G]\nName=E_Nod infantry attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602B40-G\n\n[080D8CE0-G]\nName=E_Nod infantry attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602640-G\n\n[084E90B0-G]\nName=E_Nod missile silo attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=086019D0-G\n\n[084EAF50-G]\nName=E_Nod missile silo attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0860DE90-G\n\n[084EAE80-G]\nName=E_Nod missile silo attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0846A4D0-G\n\n[0988C920-G]\nName=E_Nod missile silo attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08602640-G\n\n[084EACE0-G]\nName=E_Nod missile silo attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08602B40-G\n\n[084EAC10-G]\nName=E_Nod missile silo attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=097246D0-G\n\n[084EAB40-G]\nName=E_Nod missile silo attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08601770-G\n\n[084EAA70-G]\nName=E_Nod missile silo attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=086015E0-G\n\n[09798770-G]\nName=E_Nod power facility attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=086019D0-G\n\n[086094D0-G]\nName=E_Nod power facility attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0860DE90-G\n\n[08609400-G]\nName=E_Nod power facility attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0846A4D0-G\n\n[09798300-G]\nName=E_Nod power facility attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08602820-G\n\n[09798130-G]\nName=E_Nod power facility attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08602B40-G\n\n[09798060-G]\nName=E_Nod power facility attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=097246D0-G\n\n[09799E50-G]\nName=E_Nod power facility attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08601770-G\n\n[09799D80-G]\nName=E_Nod power facility attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=086015E0-G\n\n[09E58F50-G]\nName=E_Nod tib. refinery attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=086019D0-G\n\n[0860ADB0-G]\nName=E_Nod tib. refinery attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0860DE90-G\n\n[0860ACE0-G]\nName=E_Nod tib. refinery attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0846A4D0-G\n\n[07FC54B0-G]\nName=E_Nod tib. refinery attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08602820-G\n\n[07FC52E0-G]\nName=E_Nod tib. refinery attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08602B40-G\n\n[07FC5210-G]\nName=E_Nod tib. refinery attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=097246D0-G\n\n[07FC5040-G]\nName=E_Nod tib. refinery attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08601770-G\n\n[07FC4F50-G]\nName=E_Nod tib. refinery attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=086015E0-G\n\n[084EBC10-G]\nName=E_Nod upgrade center attack 1a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=086019D0-G\n\n[084EBB40-G]\nName=E_Nod upgrade center attack 1b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0860DE90-G\n\n[084EBA70-G]\nName=E_Nod upgrade center attack 2a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0846A4D0-G\n\n[084EB9A0-G]\nName=E_Nod upgrade center attack 2b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08602820-G\n\n[084EB8D0-G]\nName=E_Nod upgrade center attack 3a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08602B40-G\n\n[084EB800-G]\nName=E_Nod upgrade center attack 3b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=097246D0-G\n\n[084EB730-G]\nName=E_Nod upgrade center attack 4a\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08601770-G\n\n[084EB660-G]\nName=E_Nod upgrade center attack 4b\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=086015E0-G\n\n[09EF4700-G]\nName=E_Nod base defense attack 1\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=086019D0-G\n\n[09EF4630-G]\nName=E_Nod base defense attack 2\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=097246D0-G\n\n[07F3C110-G]\nName=E_Nod APC/infantry attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F3DE00-G\nTaskForce=07F3CAE0-G\n\n[07F3DAD0-G]\nName=E_GDI APC/infantry attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F3DE00-G\nTaskForce=07F3A490-G\n\n[07F3DA00-G]\nName=H_Nod APC/infantry attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F3DE00-G\nTaskForce=07F3CAE0-G\n\n[07F3D930-G]\nName=H_GDI APC/infantry attack\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=yes\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F3DE00-G\nTaskForce=07F3A490-G\n\n[084ECBA0-G]\nName=E_GDI replace MCV\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-40094\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=08B50140-G\nTaskForce=0860B440-G\n\n[084EC7F0-G]\nName=H_GDI replace MCV\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-40094\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=08B50140-G\nTaskForce=0860B440-G\n\n[084EC720-G]\nName=E_Nod replace MCV\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=1\nTechLevel=0\nGroup=-40094\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=08B50140-G\nTaskForce=0860B440-G\n\n[084EC650-G]\nName=H_Nod replace MCV\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=8\nMax=1\nTechLevel=0\nGroup=-40094\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=08B50140-G\nTaskForce=0860B440-G\n\n[080DA100-G]\nName=E_GDI infantry attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08600DC0-G\n\n[0AA4F200-G]\nName=H_GDI infantry attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=075E02F0-G\n\n[080DBF50-G]\nName=E_GDI factories attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08602640-G\n\n[080DBE80-G]\nName=E_GDI factories attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08600DC0-G\n\n[08BBE7F0-G]\nName=H_GDI factories attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07E04DC0-G\n\n[08BBE720-G]\nName=H_GDI factories attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=075E02F0-G\n\n[080DBC10-G]\nName=E_GDI vehicle attack 8\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08602640-G\n\n[080DBB40-G]\nName=E_GDI vehicle attack 9\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08600DC0-G\n\n[08BBC6B0-G]\nName=H_GDI vehicle attack 8\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07E04DC0-G\n\n[08BBC5E0-G]\nName=H_GDI vehicle attack 9\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075E02F0-G\n\n[080DB8D0-G]\nName=E_Nod factories attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08602640-G\n\n[080DB800-G]\nName=E_Nod factories attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=086015E0-G\n\n[0AA34AD0-G]\nName=H_Nod factories attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=07E04DC0-G\n\n[0AA34A00-G]\nName=H_Nod factories attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=075E2310-G\n\n[080DB590-G]\nName=E_Nod infantry attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=08602640-G\n\n[080DB4C0-G]\nName=E_Nod infantry attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=086015E0-G\n\n[0AA34190-G]\nName=H_Nod infantry attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=07E04DC0-G\n\n[0AA340C0-G]\nName=H_Nod infantry attack 6\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F76BE0-G\nTaskForce=075E2310-G\n\n[0AA35B50-G]\nName=E_Nod vehicle attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=08602640-G\n\n[0AA35A80-G]\nName=E_Nod vehicle attack 8\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=086015E0-G\n\n[0AA36F50-G]\nName=H_Nod vehicle attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=07E04DC0-G\n\n[0AA36E80-G]\nName=H_Nod vehicle attack 8\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=yes\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7E3B0-G\nTaskForce=075E2310-G\n\n[AITriggerTypes]\n08594AB0-G=H_Nod APC/engineer attack,07EB8E90-G,<all>,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,<none>,0,0,1\n085949B0-G=H_GDI APC/engineer attack,0832C790-G,<all>,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,<none>,0,0,1\n085948B0-G=H_Nod APC/eng. steal money,080646D0-G,<all>,6,4,<none>,b80b000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,<none>,0,0,1\n085947B0-G=H_GDI APC/eng. steal money,080643E0-G,<all>,6,4,<none>,b80b000004000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,<none>,0,0,1\n085946B0-G=H_Nod APC/commando attack,084D4730-G,<all>,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,<none>,0,0,1\n085945B0-G=H_GDI APC/commando attack,084D4AE0-G,<all>,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,<none>,0,0,1\n074AC080-G=H_GDI harvester attack 1,07E7F400-G,<all>,2,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07E7F400-G,0,0,1\n074ADF20-G=H_GDI harvester attack 2,0832D120-G,<all>,7,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,0832D120-G,0,0,1\n085945D0-G=H_Nod harvester attack 1,0832D7F0-G,<all>,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0832D7F0-G,0,0,1\n085944D0-G=H_Nod harvester attack 2,0832D440-G,<all>,8,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0832D440-G,0,0,1\n085940B0-G=H_Nod base repair vehicle pool,07E7F0E0-G,<all>,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,<none>,0,0,1\n08595F20-G=H_Nod base defense attack 1,07E89580-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,07E89580-G,0,0,1\n08595E20-G=H_Nod base defense attack 2,0832E770-G,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0832E770-G,0,0,1\n090EFC90-G=H_Nod ranged pool,0832E300-G,<all>,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E300-G,0,0,1\n090EFB90-G=H_Nod devil's tongue pool,07ECE560-G,<all>,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,07ECE560-G,0,0,1\n090F8F20-G=H_Nod tank pool,0832E3D0-G,<all>,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E3D0-G,0,0,1\n090F8E20-G=H_Nod stealth pool,0832E160-G,<all>,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E160-G,0,0,1\n090F8D20-G=H_GDI hover pool,090E0620-G,<all>,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,090E0620-G,0,0,1\n084EFF20-G=H_GDI aerial base attack 1,0B7D4E70-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,0,1\n08595720-G=H_GDI base defense attack 1,084B2AA0-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084B2AA0-G,0,0,1\n08595620-G=H_GDI base defense attack 2,084B2910-G,<all>,9,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084B2910-G,0,0,1\n084EFC20-G=H_GDI aerial base attack 2,0B7D4A90-G,<all>,8,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,0,1\n084EFB20-G=H_Nod aerial base attack 1,0B7D48E0-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,0,1\n084EFA20-G=H_Nod aerial base attack 2,0B7D4730-G,<all>,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,0,1\n084EF920-G=H_GDI ORCA fighter pool,0B7D4580-G,<all>,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,<none>,0,0,1\n084EF820-G=H_GDI ORCA bomber pool,0B7D44A0-G,<all>,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,<none>,0,0,1\n084EF720-G=H_Nod banshee pool,0B7D67E0-G,<all>,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,<none>,0,0,1\n084EF620-G=H_Nod harpy pool,0B7D6700-G,<all>,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,<none>,0,0,1\n08596D20-G=H_GDI vehicle attack 1,07ECD3B0-G,<all>,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07ECD3B0-G,0,0,1\n08596C20-G=H_GDI vehicle attack 2,0753BE90-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073A8540-G,0,0,1\n08596B20-G=H_GDI vehicle attack 3,07EA0540-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,<none>,0,0,1\n080DF140-G=H_GDI vehicle attack 4,084D7070-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,<none>,0,0,1\n08596850-G=H_GDI vehicle attack 5,07EA0150-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07EA0150-G,0,0,1\n08596750-G=H_GDI vehicle attack 6,07EA1E80-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0753EC30-G,0,0,1\n08596650-G=H_Nod vehicle attack 1,073A8070-G,<all>,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A8070-G,0,0,1\n08596550-G=H_Nod vehicle attack 2,073A9E50-G,<all>,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0753E780-G,0,0,1\n08596450-G=H_Nod vehicle attack 3,073A9D80-G,<all>,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A9D80-G,0,0,1\n08596350-G=H_Nod vehicle attack 4,073A9BF0-G,<all>,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0753E5F0-G,0,0,1\n084F0B20-G=H_Nod aerial vehicle attack,0B7F7500-G,<all>,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,0,1\n084F0A20-G=H_GDI aerial vehicle attack,0B7F7420-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,0,1\n08596050-G=H_Nod APC/thief steal vehicles,084D8A40-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,<none>,0,0,1\n08597E20-G=H_Nod vehicle attack 5,073A95D0-G,<all>,5,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A95D0-G,0,0,1\n08597D20-G=H_GDI infantry attack 1,073A97A0-G,<all>,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073A97A0-G,0,0,1\n08597C20-G=H_GDI infantry attack 2,073A93F0-G,<all>,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA2F0-G,0,0,1\n08597B20-G=H_GDI infantry attack 3,073A9320-G,<all>,9,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073A9320-G,0,0,1\n08597A20-G=H_GDI infantry attack 4,073A83B0-G,<all>,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA220-G,0,0,1\n08597920-G=H_Nod infantry attack 1,07EA2AC0-G,<all>,2,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07EA2AC0-G,0,0,1\n08597820-G=H_Nod infantry attack 2,07EA2930-G,<all>,4,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0753E060-G,0,0,1\n08597720-G=H_GDI vehicle attack 7,07EA22F0-G,<all>,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07EA22F0-G,0,0,1\n080DDBE0-G=H_Nod vehicle attack 6,084D9710-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,<none>,0,0,1\n08597520-G=H_Nod infantry attack 3,0965C310-G,<all>,7,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0965C310-G,0,0,1\n090F9350-G=H_Nod rocket infantry pool,074A32F0-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,074A32F0-G,0,0,1\n090FA700-G=H_Nod light infantry pool,073AB9B0-G,<all>,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,073AB9B0-G,0,0,1\n090FA600-G=H_Nod cyborg pool,084DAA80-G,<all>,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,084DAA80-G,0,0,1\n090FA500-G=H_GDI light infantry pool,090E45F0-G,<all>,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,1,090E45F0-G,0,0,1\n080AB180-G=H_GDI con. yard attack 1,084DA6A0-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,0B7F17D0-G,0,0,1\n08598F20-G=H_GDI con. yard attack 2,084DA2E0-G,<all>,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DA210-G,0,0,1\n08598E20-G=H_GDI con. yard attack 3,084DA140-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DA070-G,0,0,1\n08598D20-G=H_GDI con. yard attack 4,0B7F6CA0-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,0B7F6BC0-G,0,0,1\n08598C20-G=H_Nod con. yard attack 1,084DBDB0-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0B7F6A00-G,0,0,1\n08598B20-G=H_Nod con. yard attack 2,084DBB40-G,<all>,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0B7F6840-G,0,0,1\n085988D0-G=H_Nod con. yard attack 3,084DB9A0-G,<all>,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084DB8D0-G,0,0,1\n085987D0-G=H_Nod con. yard attack 4,084DB800-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084DB730-G,0,0,1\n085986D0-G=H_GDI factories attack 1,073AC800-G,<all>,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0B7F4B50-G,0,0,1\n085985D0-G=H_GDI factories attack 2,073AC440-G,<all>,7,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073ACE80-G,0,0,1\n085984D0-G=H_GDI factories attack 3,073ACDB0-G,<all>,9,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073AC1B0-G,0,0,1\n085983D0-G=H_GDI factories attack 4,0B7F46F0-G,<all>,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0B7F4610-G,0,0,1\n085982D0-G=H_Nod factories attack 1,09649240-G,<all>,9,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0B7F4450-G,0,0,1\n085981D0-G=H_Nod factories attack 2,0964EF50-G,<all>,5,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0B7F8070-G,0,0,1\n085980D0-G=H_Nod factories attack 3,0964EBC0-G,<all>,7,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0964EAF0-G,0,0,1\n08599F20-G=H_Nod factories attack 4,0964EA20-G,<all>,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0964E950-G,0,0,1\n08598A20-G=H_GDI infantry attack 5,073AA150-G,<all>,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA150-G,0,0,1\n0AAC5B20-G=H_GDI tib. refinery attack 1,07EF9730-G,<all>,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,04167990-G,0,0,1\n075A5EC0-G=H_GDI tib. refinery attack 2,07EA6C60-G,<all>,7,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,07EA6B90-G,0,0,1\n075A53F0-G=H_GDI tib. refinery attack 3,07EF91D0-G,<all>,9,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,075A55C0-G,0,0,1\n075A52F0-G=H_GDI tib. refinery attack 4,04174D70-G,<all>,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1\n07E8CCB0-G=H_Nod tib. refinery attack 1,07E89DD0-G,<all>,9,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,04176E60-G,0,0,1\n07E8CBB0-G=H_Nod tib. refinery attack 2,073AEA00-G,<all>,5,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,04173E00-G,0,0,1\n07E8CAB0-G=H_Nod tib. refinery attack 3,07E8F140-G,<all>,7,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07E8CF50-G,0,0,1\n07E8C9B0-G=H_Nod tib. refinery attack 4,073AE6C0-G,<all>,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07E8CDB0-G,0,0,1\n084712B0-G=H_GDI power facility attack 1,073AE790-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,041742A0-G,0,0,1\n08471C20-G=H_GDI power facility attack 2,073AEE80-G,<all>,7,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AE260-G,0,0,1\n08472F20-G=H_GDI power facility attack 3,073AE190-G,<all>,9,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AE0C0-G,0,0,1\n08472E20-G=H_GDI power facility attack 4,041768B0-G,<all>,8,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,041767D0-G,0,0,1\n08472D20-G=H_Nod power facility attack 1,0A9BBF50-G,<all>,9,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,04181EE0-G,0,0,1\n08472C20-G=H_Nod power facility attack 2,0A9BBDB0-G,<all>,5,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,04181D20-G,0,0,1\n08472B20-G=H_Nod power facility attack 3,0A9BBC10-G,<all>,7,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A9BBB40-G,0,0,1\n08472A20-G=H_Nod power facility attack 4,0A9BBA70-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A9BB9A0-G,0,0,1\n0859AB20-G=H_Nod infantry attack 4,073AA970-G,<all>,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,073AA220-G,0,0,1\n090FDF20-G=H_GDI amphibious pool,090E0AC0-G,<all>,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E0AC0-G,0,0,1\n090FDE20-G=H_GDI disc thrower pool,090E4F50-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E4F50-G,0,0,1\n090FC820-G=H_GDI disruptor pool,090E2C20-G,<all>,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,090E2C20-G,0,0,1\n090FDB60-G=H_GDI jumpjet infantry pool,090E4520-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E4520-G,0,0,1\n090FDA60-G=H_GDI titan pool,090E0930-G,<all>,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,090E0930-G,0,0,1\n090FD960-G=H_GDI wolverine pool,090E07A0-G,<all>,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E07A0-G,0,0,1\n090FD860-G=H_Nod recon pool 2,073AF270-G,<all>,2,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,073AF270-G,0,0,1\n090FD760-G=H_Nod subterranean APC pool,073AF400-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,073AF400-G,0,0,1\n0859A160-G=H_GDI missile silo attack 1,084DE340-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080F71A0-G,0,0,1\n0859A060-G=H_GDI missile silo attack 2,084DFF50-G,<all>,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084DEA70-G,0,0,1\n0859BF20-G=H_GDI missile silo attack 3,084DE9A0-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084DFBF0-G,0,0,1\n0859BE20-G=H_GDI missile silo attack 4,080CCD80-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CCCA0-G,0,0,1\n0859A820-G=H_Nod missile silo attack 1,084DF980-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CCAE0-G,0,0,1\n0859BB30-G=H_Nod missile silo attack 2,084DF7E0-G,<all>,5,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,0,1\n0859BA30-G=H_Nod missile silo attack 3,084DF640-G,<all>,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084DFE80-G,0,0,1\n0859B930-G=H_Nod missile silo attack 4,084DFDB0-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084DFCE0-G,0,0,1\n084F4230-G=H_Nod engineer pool ,084DF2C0-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,0,1\n084F4130-G=H_GDI engineer pool,084DF1F0-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,0,1\n0859B630-G=H_Nod mutant hijacker pool,084DF120-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,0,1\n074B3330-G=H_GDI ghostalker pool,084DF050-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,0,1\n0859B430-G=H_Nod cyborg commando pool,084D7830-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,0,1\n0859B330-G=H_GDI upgrade center attack 1,084E0F50-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CEA80-G,0,0,1\n0859BD20-G=H_GDI upgrade center attack 2,084E08B0-G,<all>,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084E07E0-G,0,0,1\n0859CF20-G=H_GDI upgrade center attack 3,084E0710-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084E0640-G,0,0,1\n0859CE20-G=H_GDI upgrade center attack 4,080CE620-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CE540-G,0,0,1\n0859CD20-G=H_Nod upgrade center attack 1,084E03D0-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CE380-G,0,0,1\n0859CC20-G=H_Nod upgrade center attack 2,084E0230-G,<all>,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CE1C0-G,0,0,1\n0859CB20-G=H_Nod upgrade center attack 3,084E0090-G,<all>,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084E0B90-G,0,0,1\n0859CA20-G=H_Nod upgrade center attack 4,084E0AC0-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084E09F0-G,0,0,1\n0859C920-G=E_GDI vehicle attack 1,080CF910-G,<all>,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF910-G,1,0,0\n0859C820-G=E_GDI vehicle attack 2,080CF780-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF6B0-G,1,0,0\n0859C720-G=E_GDI vehicle attack 3,080CF520-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0\n0AF3EF10-G=E_GDI vehicle attack 4,084E1790-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,<none>,1,0,0\n0859B130-G=E_GDI vehicle attack 5,080CF380-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF380-G,1,0,0\n0859B030-G=E_GDI vehicle attack 6,080CFC20-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080C70C0-G,1,0,0\n0859C3E0-G=E_GDI vehicle attack 7,080D0D90-G,<all>,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D0D90-G,1,0,0\n0859C2E0-G=E_Nod vehicle attack 1,08468050-G,<all>,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08468050-G,1,0,0\n0859C1E0-G=E_Nod vehicle attack 2,07D73AA0-G,<all>,3,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07D73840-G,1,0,0\n0859C0E0-G=E_Nod vehicle attack 3,07D739D0-G,<all>,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07D739D0-G,1,0,0\n0859DF20-G=E_Nod vehicle attack 4,07E4EA50-G,<all>,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07E4CF50-G,1,0,0\n0859DE20-G=E_Nod vehicle attack 5,086012B0-G,<all>,5,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,086012B0-G,1,0,0\n080E2360-G=E_Nod vehicle attack 6,084E1530-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,1,0,0\n07E8BE50-G=H_Nod base air defense 1,084E3130-G,<all>,8,0,SCRIN,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1\n07E8BD50-G=H_Nod base air defense 2,084E3130-G,<all>,8,0,ORCAB,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1\n07E8BC50-G=H_Nod base air defense 3,084E3130-G,<all>,8,0,APACHE,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1\n07E8BB50-G=H_Nod base air defense 4,084E3130-G,<all>,8,0,ORCA,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1\n084756C0-G=H_GDI base air defense 1,084E4F50-G,<all>,7,0,SCRIN,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1\n07E8B410-G=H_GDI base air defense 2,084E4F50-G,<all>,7,0,ORCAB,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1\n07E8B310-G=H_GDI base air defense 3,084E4F50-G,<all>,7,0,APACHE,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1\n07E8B210-G=H_GDI base air defense 4,084E4F50-G,<all>,7,0,ORCA,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1\n084F7D20-G=E_Nod banshee pool,084E1460-G,<all>,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,1,0,0\n0859D2C0-G=E_Nod base repair vehicle pool,097235F0-G,<all>,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,1,0,0\n07D5E8A0-G=E_Nod cyborg commando pool,084E21C0-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,<none>,1,0,0\n09100F20-G=E_Nod cyborg pool,090EDA60-G,<all>,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDA60-G,1,0,0\n07F232E0-G=E_Nod engineer pool,084E3E80-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,<none>,1,0,0\n09101F20-G=E_Nod devil's tongue pool,090ED990-G,<all>,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED990-G,1,0,0\n0859ED20-G=E_Nod harpy pool,084E3CF0-G,<all>,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,1,0,0\n09101D20-G=E_Nod light infantry pool,090ECA90-G,<all>,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090ECA90-G,1,0,0\n07D70D20-G=E_Nod mutant hijacker pool,084E3980-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,<none>,1,0,0\n09101B20-G=E_Nod ranged pool,090ED230-G,<all>,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED230-G,1,0,0\n09101A20-G=E_Nod recon pool 1,08602F50-G,<all>,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08602F50-G,1,0,0\n09101920-G=E_Nod recon pool 2,090EDE80-G,<all>,2,1,NAWEAP,0000000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDE80-G,1,0,0\n091002C0-G=E_Nod rocket infantry pool,090EDCF0-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDCF0-G,1,0,0\n091001C0-G=E_Nod stealth pool,090EDC20-G,<all>,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090EDC20-G,1,0,0\n07D70470-G=E_Nod subterranean APC pool,084E32D0-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,1,0,0\n09101470-G=E_Nod tank pool,090EEB00-G,<all>,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,090EEB00-G,1,0,0\n0859E270-G=E_GDI aerial base attack 1,07F2A9F0-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07F2A9F0-G,1,0,0\n0859E170-G=E_GDI aerial base attack 2,07F2A920-G,<all>,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07EF88B0-G,1,0,0\n0859E070-G=E_GDI aerial vehicle attack,07F2A850-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07F2A850-G,1,0,0\n0859FF20-G=E_GDI APC/commando attack,084E5E80-G,<all>,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,<none>,1,0,0\n0859FE20-G=E_GDI APC/eng. steal money,07EF7DF0-G,<all>,6,4,<none>,8813000004000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,<none>,1,0,0\n0859FD20-G=E_GDI APC/engineer attack,07EF7D20-G,<all>,6,-1,ENGINEER,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,<none>,1,0,0\n07EF8B30-G=E_GDI base air defense 1,084E5B50-G,<all>,7,0,SCRIN,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n07EF8A30-G=E_GDI base air defense 2,084E5B50-G,<all>,7,0,ORCAB,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n0859F940-G=E_GDI base air defense 3,084E5B50-G,<all>,7,0,APACHE,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n0859F840-G=E_GDI base air defense 4,084E5B50-G,<all>,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n0859F740-G=E_GDI base defense attack 1,07EFA790-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07EFA790-G,1,0,0\n0859F640-G=E_GDI base defense attack 2,07EF88B0-G,<all>,9,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07EF88B0-G,1,0,0\n0859F540-G=E_GDI con. yard attack 1,084E43A0-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E42D0-G,1,0,0\n0859F440-G=E_GDI con. yard attack 2,084E55C0-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E54F0-G,1,0,0\n0859F340-G=E_GDI con. yard attack 3,084E5420-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E5350-G,1,0,0\n0859F240-G=E_GDI con. yard attack 4,084E5280-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E51B0-G,1,0,0\n096E8880-G=E_GDI amphibious pool,084E37E0-G,<all>,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,1,0,0\n09102C20-G=E_GDI disc thrower pool,090EEDB0-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EEDB0-G,1,0,0\n09103F20-G=E_GDI disruptor pool,090EECE0-G,<all>,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EECE0-G,1,0,0\n096E8580-G=E_GDI engineer pool,084E4AE0-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,<none>,1,0,0\n096E8480-G=E_GDI ghostalker pool,084E4A10-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,<none>,1,0,0\n09103C20-G=E_GDI hover pool,090EE280-G,<all>,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0\n09103B20-G=E_GDI jumpjet infantry pool,090EE1B0-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EE1B0-G,1,0,0\n09103A20-G=E_GDI light infantry pool,090F0340-G,<all>,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090F0340-G,1,0,0\n085A0420-G=E_GDI ORCA bomber pool,084E47A0-G,<all>,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,1,0,0\n085A0320-G=E_GDI ORCA fighter pool,084E4610-G,<all>,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,1,0,0\n09102240-G=E_GDI titan pool,090EE700-G,<all>,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE700-G,1,0,0\n09102140-G=E_GDI wolverine pool ,090EE630-G,<all>,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EE630-G,1,0,0\n085A0F20-G=E_GDI factories attack 1,080D32A0-G,<all>,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D31D0-G,1,0,0\n085A0E20-G=E_GDI factories attack 2,080D3100-G,<all>,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D3030-G,1,0,0\n085A0D20-G=E_GDI factories attack 3,080D4A70-G,<all>,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D49A0-G,1,0,0\n085A1BF0-G=E_GDI factories attack 4,080D48D0-G,<all>,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D4800-G,1,0,0\n07E30660-G=E_GDI harvester attack 1,07E30830-G,<all>,2,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07E30830-G,1,0,0\n07E30560-G=E_GDI harvester attack 2,07E30760-G,<all>,7,0,HORV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07E30760-G,1,0,0\n085A18F0-G=E_GDI infantry attack 1,080D4590-G,<all>,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4590-G,1,0,0\n085A17F0-G=E_GDI infantry attack 2,080D44C0-G,<all>,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D43F0-G,1,0,0\n085A1F20-G=E_GDI infantry attack 3,080D4320-G,<all>,9,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4320-G,1,0,0\n085A1E20-G=E_GDI infantry attack 4,080D4250-G,<all>,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4180-G,1,0,0\n085A1D20-G=E_GDI infantry attack 5,080D40B0-G,<all>,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D40B0-G,1,0,0\n085A13A0-G=E_GDI missile silo attack 1,084E64C0-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E63F0-G,1,0,0\n085A12A0-G=E_GDI missile silo attack 2,084E6320-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E6250-G,1,0,0\n085A11A0-G=E_GDI missile silo attack 3,084E6180-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E60B0-G,1,0,0\n085A10A0-G=E_GDI missile silo attack 4,084E7F50-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E7E80-G,1,0,0\n0856AF20-G=E_GDI power facility attack 1,080D58D0-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D5800-G,1,0,0\n07E57AE0-G=E_GDI power facility attack 2,080D5730-G,<all>,9,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D5660-G,1,0,0\n0856AD20-G=E_GDI power facility attack 3,080D5590-G,<all>,9,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,080D54C0-G,1,0,0\n085696F0-G=E_GDI power facility attack 4,080D53F0-G,<all>,8,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,080D5320-G,1,0,0\n07ECC820-G=E_GDI tib. refinery attack 1,07ECC9F0-G,<all>,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC920-G,1,0,0\n07ECC580-G=E_GDI tib. refinery attack 2,07ECC750-G,<all>,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC680-G,1,0,0\n07ECC2E0-G=E_GDI tib. refinery attack 3,07ECC4B0-G,<all>,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC3E0-G,1,0,0\n07ECC040-G=E_GDI tib. refinery attack 4,07ECC210-G,<all>,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC140-G,1,0,0\n085A26A0-G=E_GDI upgrade center attack 1,084E70B0-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084D9250-G,1,0,0\n085A25A0-G=E_GDI upgrade center attack 2,084DF570-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DF4A0-G,1,0,0\n085A24A0-G=E_GDI upgrade center attack 3,084DF3D0-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E8F50-G,1,0,0\n085A23A0-G=E_GDI upgrade center attack 4,084E8E80-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E8DB0-G,1,0,0\n085A22A0-G=E_Nod aerial base attack 1,09D00530-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,09D00530-G,1,0,0\n085A21A0-G=E_Nod aerial base attack 2,08607590-G,<all>,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,08607590-G,1,0,0\n085A20A0-G=E_Nod aerial vehicle attack ,086074C0-G,<all>,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086074C0-G,1,0,0\n085A2C20-G=E_Nod APC/commando attack,084E8A70-G,<all>,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,<none>,1,0,0\n085A2B20-G=E_Nod APC/eng. steal money ,07E8D320-G,<all>,6,4,<none>,8813000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,<none>,1,0,0\n085A2A20-G=E_Nod APC/engineer attack,07E8D250-G,<all>,6,-1,ENGINEER,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,<none>,1,0,0\n085A3B80-G=E_Nod APC/thief steal vehicles,084E8740-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,<none>,1,0,0\n07EA54C0-G=E_Nod base air defense 1,084E8670-G,<all>,8,0,SCRIN,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0\n07EA53C0-G=E_Nod base air defense 2,084E8670-G,<all>,8,0,ORCAB,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0\n085A3880-G=E_Nod base air defense 3,084E8670-G,<all>,8,0,APACHE,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0\n085A3780-G=E_Nod base air defense 4,084E8670-G,<all>,8,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0\n085A32B0-G=E_Nod con. yard attack 1,084E84D0-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E8400-G,1,0,0\n085A31B0-G=E_Nod con. yard attack 2,084E8330-G,<all>,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E8260-G,1,0,0\n085A30B0-G=E_Nod con. yard attack 3,084E8190-G,<all>,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E80C0-G,1,0,0\n085A3F20-G=E_Nod con. yard attack 4,084E9F50-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E9E80-G,1,0,0\n085A3E20-G=E_Nod factories attack 1,080D7590-G,<all>,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D74C0-G,1,0,0\n085A3D20-G=E_Nod factories attack 2,080D73F0-G,<all>,5,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D7320-G,1,0,0\n085A4F20-G=E_Nod factories attack 3,080D7250-G,<all>,7,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D7180-G,1,0,0\n085A4E20-G=E_Nod factories attack 4,080D70B0-G,<all>,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080C8F50-G,1,0,0\n085A4D20-G=E_Nod harvester attack 1,086083F0-G,<all>,5,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086083F0-G,1,0,0\n085A4C20-G=E_Nod harvester attack 2,09D6EF50-G,<all>,8,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09D6EF50-G,1,0,0\n085A3680-G=E_Nod infantry attack 1,080CF110-G,<all>,2,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080CF110-G,1,0,0\n085A3580-G=E_Nod infantry attack 2,07FCFC60-G,<all>,4,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,07FCFB90-G,1,0,0\n085A3480-G=E_Nod infantry attack 3,07FCFAC0-G,<all>,7,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,07FCFAC0-G,1,0,0\n085A4730-G=E_Nod infantry attack 4,08750120-G,<all>,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080D8CE0-G,1,0,0\n085A4630-G=E_Nod missile silo attack 1,084E90B0-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAF50-G,1,0,0\n085A4530-G=E_Nod missile silo attack 2,084EAE80-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0988C920-G,1,0,0\n085A4430-G=E_Nod missile silo attack 3,084EACE0-G,<all>,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAC10-G,1,0,0\n097985A0-G=E_Nod power facility attack 1,09798770-G,<all>,9,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086094D0-G,1,0,0\n085A4230-G=E_Nod missile silo attack 4,084EAB40-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAA70-G,1,0,0\n09798200-G=E_Nod power facility attack 2,08609400-G,<all>,5,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09798300-G,1,0,0\n09799F20-G=E_Nod power facility attack 3,09798130-G,<all>,7,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09798060-G,1,0,0\n09799C80-G=E_Nod power facility attack 4,09799E50-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09799D80-G,1,0,0\n07FC5650-G=E_Nod tib. refinery attack 1,09E58F50-G,<all>,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,0860ADB0-G,1,0,0\n07FC53B0-G=E_Nod tib. refinery attack 2,0860ACE0-G,<all>,5,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC54B0-G,1,0,0\n07FC5110-G=E_Nod tib. refinery attack 3,07FC52E0-G,<all>,7,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC5210-G,1,0,0\n07FC4E50-G=E_Nod tib. refinery attack 4,07FC5040-G,<all>,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC4F50-G,1,0,0\n085A5D20-G=E_Nod upgrade center attack 1,084EBC10-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EBB40-G,1,0,0\n085A5C20-G=E_Nod upgrade center attack 2,084EBA70-G,<all>,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB9A0-G,1,0,0\n085A5B20-G=E_Nod upgrade center attack 3,084EB8D0-G,<all>,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB800-G,1,0,0\n085A5A20-G=E_Nod upgrade center attack 4,084EB730-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB660-G,1,0,0\n080CF220-G=M_GDI aerial base attack 1,0B7D4E70-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n080CF120-G=M_GDI aerial base attack 2,0B7D4A90-G,<all>,8,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n080C71E0-G=M_GDI aerial vehicle attack,0B7F7420-G,<all>,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080C70E0-G=M_GDI amphibious pool,090E0AC0-G,<all>,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n085A5520-G=M_GDI APC/commando attack,084D4AE0-G,<all>,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n085A5420-G=M_GDI APC/eng. steal money,080643E0-G,<all>,6,4,ENGINEER,a00f000004000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,1,<none>,0,1,0\n085A5320-G=M_GDI APC/engineer attack,0832C790-G,<all>,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D0C20-G=M_GDI base air defense 1,084E4F50-G,<all>,7,0,SCRIN,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0\n080D0B20-G=M_GDI base air defense 2,084E38B0-G,<all>,7,0,ORCAB,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E4F50-G,0,1,0\n080D0A20-G=M_GDI base air defense 4,084E38B0-G,<all>,7,0,ORCA,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E4F50-G,0,1,0\n080D0920-G=M_GDI base defense attack 1,084B2AA0-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D0820-G=M_GDI base defense attack 2,084B2910-G,<all>,9,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D0720-G=M_GDI con. yard attack 1,084DA6A0-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,0B7F17D0-G,0,1,0\n080D0620-G=M_GDI con. yard attack 2,084DA2E0-G,<all>,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n080D0520-G=M_GDI con. yard attack 3,084DA140-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,084DA070-G,0,1,0\n080D0420-G=M_GDI con. yard attack 4,0B7F6CA0-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,0B7F6BC0-G,0,1,0\n080D0320-G=M_GDI disc thrower pool,090E4F50-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n080D0220-G=M_GDI disruptor pool,090E2C20-G,<all>,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D0120-G=M_GDI engineer pool,084DF1F0-G,<all>,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1F20-G=M_GDI factories attack 1,073AC800-G,<all>,8,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0B7F4B50-G,0,1,0\n080D1E20-G=M_GDI factories attack 2,073AC440-G,<all>,7,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073ACE80-G,0,1,0\n080D1D20-G=M_GDI factories attack 3,073ACDB0-G,<all>,9,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073AC1B0-G,0,1,0\n080D1C20-G=M_GDI factories attack 4,0B7F46F0-G,<all>,8,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0B7F4610-G,0,1,0\n07F26720-G=M_GDI ghoststalker pool,084DF050-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1A20-G=M_GDI harvester attack 1,07E7F400-G,<all>,2,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1920-G=M_GDI harvester attack 2,0832D120-G,<all>,7,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1820-G=M_GDI hover pool,090E0620-G,<all>,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D1720-G=M_GDI infantry attack 1,073A97A0-G,<all>,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1620-G=M_GDI infantry attack 2,073A93F0-G,<all>,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AA2F0-G,0,1,0\n080D1520-G=M_GDI infantry attack 3,073A9320-G,<all>,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1420-G=M_GDI infantry attack 4,073A83B0-G,<all>,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AA220-G,0,1,0\n080D1320-G=M_GDI infantry attack 5,073AA150-G,<all>,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D1220-G=M_GDI jumpjet infantry pool,090E4520-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n080D1120-G=M_GDI light infantry pool,090E45F0-G,<all>,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n080D2F20-G=M_GDI missile silo attack 1,084DE340-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080F71A0-G,0,1,0\n080D2E20-G=M_GDI missile silo attack 2,084DFF50-G,<all>,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084DEA70-G,0,1,0\n080D2D20-G=M_GDI missile silo attack 3,084DE9A0-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084DFBF0-G,0,1,0\n080D2C20-G=M_GDI missile silo attack 4,080CCD80-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CCCA0-G,0,1,0\n08597420-G=M_GDI ORCA bomber pool,0B7D44A0-G,<all>,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n0859F140-G=M_GDI ORCA fighter pool,0B7D4580-G,<all>,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D2920-G=M_GDI power facility attack 1,073AE790-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041742A0-G,0,1,0\n080D2820-G=M_GDI power facility attack 2,073AEE80-G,<all>,7,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AE260-G,0,1,0\n080D2720-G=M_GDI power facility attack 3,073AE190-G,<all>,9,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AE0C0-G,0,1,0\n080D2620-G=M_GDI power facility attack 4,041768B0-G,<all>,8,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041767D0-G,0,1,0\n080D2520-G=M_GDI tib. refinery attack 1,07EF9730-G,<all>,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04167990-G,0,1,0\n080D2420-G=M_GDI tib. refinery attack 2,07EA6C60-G,<all>,7,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,07EA6B90-G,0,1,0\n080D2320-G=M_GDI tib. refinery attack 3,07EF91D0-G,<all>,9,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04174D70-G,0,1,0\n080D2220-G=M_GDI tib. refinery attack 4,04174D70-G,<all>,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,1,0\n080D2120-G=M_GDI titan pool,090E0930-G,<all>,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D3F20-G=M_GDI upgrade center attack 1,084E0F50-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CEA80-G,0,1,0\n080D3E20-G=M_GDI upgrade center attack 2,084E08B0-G,<all>,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084E07E0-G,0,1,0\n080D3D20-G=M_GDI upgrade center attack 3,084E0710-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084E0640-G,0,1,0\n080D3C20-G=M_GDI upgrade center attack 4,080CE620-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CE540-G,0,1,0\n080D3B20-G=M_GDI vehicle attack 1,07ECD3B0-G,<all>,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D3A20-G=M_GDI vehicle attack 2,0753BE90-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073A8540-G,0,1,0\n080D3920-G=M_GDI vehicle attack 3,07EA0540-G,<all>,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080F9470-G=M_GDI vehicle attack 4,084D7070-G,<all>,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D3720-G=M_GDI vehicle attack 5,07EA0150-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080D3620-G=M_GDI vehicle attack 6,07EA1E80-G,<all>,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0753EC30-G,0,1,0\n080D3520-G=M_GDI vehicle attack 7,07EA22F0-G,<all>,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n08500120-G=M_GDI wolverine pool ,090E07A0-G,<all>,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n0914DE20-G=H_Nod recon pool 1,0832E230-G,<all>,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0832E230-G,0,0,1\n09EF4530-G=E_Nod base defense attack 1,09EF4700-G,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,09EF4700-G,1,0,0\n09EF4430-G=E_Nod base defense attack 2,09EF4630-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,09EF4630-G,1,0,0\n080D4F20-G=M_Nod aerial base attack 1,0B7D48E0-G,<all>,5,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080D4E20-G=M_Nod aerial base attack 2,0B7D4730-G,<all>,9,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080D4D20-G=M_Nod aerial vehicle attack,0B7F7500-G,<all>,9,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n09F30510-G=M_Nod APC/commando attack,084D4730-G,<all>,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n09F30410-G=M_Nod APC/eng. steal money,080646D0-G,<all>,6,4,<none>,a00f000004000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n09F30310-G=M_Nod APC/engineer attack,07EB8E90-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n09F30210-G=M_Nod APC/thief steal vehicles,084D8A40-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n080D4820-G=M_Nod base air defense 1,084E3130-G,<all>,8,0,SCRIN,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0\n080D4720-G=M_Nod base air defense 2,084E3060-G,<all>,8,0,ORCAB,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3130-G,0,1,0\n080D4620-G=M_Nod base air defense 3,084E3130-G,<all>,8,0,APACHE,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0\n080D4520-G=M_Nod base air defense 4,084E3060-G,<all>,8,0,ORCA,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3130-G,0,1,0\n080D4420-G=M_Nod base defense attack 1,07E89580-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080D4320-G=M_Nod base defense attack 2,0832E770-G,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080D4220-G=M_Nod con. yard attack 1,084DBDB0-G,<all>,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,0B7F6A00-G,0,1,0\n080D4120-G=M_Nod con. yard attack 2,084DBB40-G,<all>,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,0B7F6840-G,0,1,0\n080D5F20-G=M_Nod con. yard attack 3,084DB9A0-G,<all>,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,084DB8D0-G,0,1,0\n080D5E20-G=M_Nod con. yard attack 4,084DB800-G,<all>,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,084DB730-G,0,1,0\n080D5D20-G=M_Nod factories attack 1,09649240-G,<all>,8,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0B7F4450-G,0,1,0\n080D5C20-G=M_Nod factories attack 2,0964EF50-G,<all>,5,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0B7F8070-G,0,1,0\n080D5B20-G=M_Nod factories attack 3,0964EBC0-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0964EAF0-G,0,1,0\n080D5A20-G=M_Nod factories attack 4,0964EA20-G,<all>,8,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0964E950-G,0,1,0\n080D5920-G=M_Nod harvester attack 1,0832D7F0-G,<all>,5,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D5820-G=M_Nod harvester attack 2,0832D440-G,<all>,8,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D5720-G=M_Nod infantry attack 1,07EA2AC0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D5620-G=M_Nod infantry attack 2,07EA2930-G,<all>,4,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0753E060-G,0,1,0\n080D5520-G=M_Nod infantry attack 3,0965C310-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D5420-G=M_Nod infantry attack 4,073AA970-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0753E130-G,0,1,0\n080D5320-G=M_Nod missile silo attack 1,084DF980-G,<all>,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CCAE0-G,0,1,0\n080D5220-G=M_Nod missile silo attack 2,084DF7E0-G,<all>,5,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,1,0\n080D5120-G=M_Nod missile silo attack 3,084DF640-G,<all>,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084DFE80-G,0,1,0\n080D6F20-G=M_Nod missile silo attack 4,084DFDB0-G,<all>,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084DFCE0-G,0,1,0\n080D6E20-G=M_Nod power facility attack 1,0A9BBF50-G,<all>,9,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,04181EE0-G,0,1,0\n080D6D20-G=M_Nod power facility attack 2,0A9BBDB0-G,<all>,5,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,04181D20-G,0,1,0\n080D6C20-G=M_Nod power facility attack 3,0A9BBC10-G,<all>,7,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0A9BBB40-G,0,1,0\n080D6B20-G=M_Nod power facility attack 4,0A9BBA70-G,<all>,8,2,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0A9BB9A0-G,0,1,0\n080D6A20-G=M_Nod tib. refinery attack 1,07E89DD0-G,<all>,9,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,04176E60-G,0,1,0\n080D6920-G=M_Nod tib. refinery attack 2,073AEA00-G,<all>,5,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,04173E00-G,0,1,0\n080D6820-G=M_Nod tib. refinery attack 3,07E8F140-G,<all>,7,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,07E8CF50-G,0,1,0\n080D6720-G=M_Nod tib. refinery attack 4,073AE6C0-G,<all>,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,07E8CDB0-G,0,1,1\n080D6620-G=M_Nod upgrade center attack 1,084E03D0-G,<all>,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE380-G,0,1,0\n080D6520-G=M_Nod upgrade center attack 2,084E0230-G,<all>,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE1C0-G,0,1,0\n080D6420-G=M_Nod upgrade center attack 3,084E0090-G,<all>,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084E0B90-G,0,1,0\n080D6320-G=M_Nod upgrade center attack 4,084E0AC0-G,<all>,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084E09F0-G,0,1,0\n080D6220-G=M_Nod vehicle attack 1,073A8070-G,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080D6120-G=M_Nod vehicle attack 2,073A9E50-G,<all>,3,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0753E780-G,0,1,0\n080D7F20-G=M_Nod vehicle attack 3,073A9D80-G,<all>,8,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080D7E20-G=M_Nod vehicle attack 4,073AA970-G,<all>,7,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0753E5F0-G,0,1,0\n080D7D20-G=M_Nod vehicle attack 5,073A95D0-G,<all>,5,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080FB470-G=M_Nod vehicle attack 6,084D9710-G,<all>,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n09F2EB20-G=M_Nod banshee pool,0B7D67E0-G,<all>,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n09F2EA20-G=M_Nod base repair vehicle pool,07E7F0E0-G,<all>,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n09F2E920-G=M_Nod cyborg commando pool,084D7830-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080D7820-G=M_Nod cyborg pool,084DAA80-G,<all>,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080D7720-G=M_Nod devil's tongue pool,07ECE560-G,<all>,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080D7620-G=M_Nod engineer pool,084DF2C0-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n09F2E520-G=M_Nod harpy pool,0B7D6700-G,<all>,5,1,NAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n09A92A20-G=M_Nod light infantry pool,073AB9B0-G,<all>,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n09F2E320-G=M_Nod mutant hijacker pool,084DF120-G,<all>,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080E8F20-G=M_Nod ranged pool ,0832E300-G,<all>,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080E8E20-G=M_Nod recon pool 1,0832E230-G,<all>,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080E8D20-G=M_Nod recon pool 2,073AF270-G,<all>,2,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080E8C20-G=M_Nod rocket infantry pool,074A32F0-G,<all>,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080E8B20-G=M_Nod stealth pool,0832E160-G,<all>,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080E8A20-G=M_Nod subterranean APC pool,073AF400-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,<none>,0,1,0\n080E8920-G=M_Nod tank pool,0832E3D0-G,<all>,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n07F3D830-G=E_GDI APC/infantry attack,07F3DAD0-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07F3DAD0-G,1,0,0\n080D8920-G=M_GDI APC/infantry attack,07F3D930-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n07F3EF20-G=H_GDI APC/infantry attack,07F3D930-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07F3D930-G,0,0,1\n07F3EE20-G=E_Nod APC/infantry attack,07F3C110-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,70.000000,1,0,2,0,07F3C110-G,1,0,0\n080D8620-G=M_Nod APC/infantry attack,07F3DA00-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n07F3EC20-G=H_Nod APC/infantry attack,07F3DA00-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07F3DA00-G,0,0,1\n08507720-G=E_GDI replace MCV,084ECBA0-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,1,0,0\n08B80760-G=H_GDI replace MCV,084EC7F0-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,<none>,0,0,1\n08B80660-G=M_GDI replace MCV,084EC7F0-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,<none>,0,1,0\n091F4720-G=E_Nod replace MCV,084EC720-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,1,0,0\n0930BDC0-G=H_Nod replace MCV,084EC650-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,<none>,0,0,1\n0930A240-G=M_Nod replace MCV,084EC650-G,<all>,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,<none>,0,1,0\n0AA4F100-G=E_GDI infantry attack 6,080DA100-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080DA100-G,1,0,0\n0AA50F20-G=H_GDI infantry attack 6,0AA4F200-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,0AA4F200-G,0,0,1\n08BBE9C0-G=E_GDI factories attack 5,080DBF50-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBF50-G,1,0,0\n08BBE8C0-G=E_GDI factories attack 6,080DBE80-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBE80-G,1,0,0\n08BBCF20-G=H_GDI factories attack 5,08BBE7F0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBE7F0-G,0,0,1\n08BBCE20-G=H_GDI factories attack 6,08BBE720-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBE720-G,0,0,1\n08BBCB80-G=E_GDI vehicle attack 8,080DBC10-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBC10-G,1,0,0\n08BBCA80-G=E_GDI vehicle attack 9,080DBB40-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBB40-G,1,0,0\n080D9520-G=M_GDI infantry attack 6,0AA4F200-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,<none>,0,1,0\n080D9420-G=M_GDI factories attack 5,08BBE7F0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,0,0,<none>,0,1,0\n080D9320-G=M_GDI factories attack 6,08BBE720-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n08BBC4E0-G=H_GDI vehicle attack 8,08BBC6B0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBC6B0-G,0,0,1\n08BBC3E0-G=H_GDI vehicle attack 9,08BBC5E0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBC5E0-G,0,0,1\n080DAF20-G=M_GDI vehicle attack 8,08BBC6B0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n080DAE20-G=M_GDI vehicle attack 9,08BBC5E0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n0AA34CA0-G=E_Nod factories attack 5,080DB8D0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080DB8D0-G,1,0,0\n0AA34BA0-G=E_Nod factories attack 6,080DB800-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080DB800-G,1,0,0\n0AA34900-G=H_Nod factories attack 5,0AA34AD0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA34AD0-G,0,0,1\n0AA34800-G=H_Nod factories attack 6,0AA34A00-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA34A00-G,0,0,1\n080DA920-G=M_Nod factories attack 5,0AA34AD0-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080DA820-G=M_Nod factories attack 6,0AA34A00-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n0AA34360-G=E_Nod infantry attack 5,080DB590-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080DB590-G,1,0,0\n0AA34260-G=E_Nod infantry attack 6,080DB4C0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080DB4C0-G,1,0,0\n0AA35F20-G=H_Nod infantry attack 5,0AA34190-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA34190-G,0,0,1\n0AA35E20-G=H_Nod infantry attack 6,0AA340C0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA340C0-G,0,0,1\n080DA320-G=M_Nod infantry attack 5,0AA34190-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n080DA220-G=M_Nod infantry attack 6,0AA340C0-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n0AA35980-G=E_Nod vehicle attack 7,0AA35B50-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA35B50-G,1,0,0\n0AA35070-G=E_Nod vehicle attack 8,0AA35A80-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA35A80-G,1,0,0\n0AA36D80-G=H_Nod vehicle attack 7,0AA36F50-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA36F50-G,0,0,1\n0AA36C80-G=H_Nod vehicle attack 8,0AA36E80-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA36E80-G,0,0,1\n080DBC20-G=M_Nod vehicle attack 7,0AA36F50-G,<all>,1,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n080DBB20-G=M_Nod vehicle attack 8,0AA36E80-G,<all>,2,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n09EEBCD0-G=M_GDI base air defense 3,084E4F50-G,<all>,7,0,APACHE,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0\n08440AE0-G=E_GDI base air defense 5,084E5B50-G,<all>,7,0,JUMPJET,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0\n08440950-G=E_Nod base air defense 5,084E8670-G,<all>,8,0,JUMPJET,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0\n08440850-G=M_GDI base air defense 5,084E4F50-G,<all>,7,0,JUMPJET,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0\n08440750-G=M_Nod base air defense 5,084E3130-G,<all>,8,0,JUMPJET,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0\n08440650-G=H_GDI base air defense 5,084E4F50-G,<all>,7,0,JUMPJET,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1\n08440550-G=H_Nod base air defense 5,084E3130-G,<all>,8,0,JUMPJET,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1\n\n[Digest]\n1=aG6xg9lMqJoMyUqKRzv4QJZ2vmk=\n"
  },
  {
    "path": "DXMainClient/Resources/INI/aifs.ini",
    "content": "[TaskForces]\n1000=08820130-G\n1001=08822830-G\n1002=0A70DB10-G\n1003=0A70E6A0-G\n1004=0832C3F0-G\n1005=07ECC200-G\n1006=08063D20-G\n1007=08063DE0-G\n1008=0804C6C0-G\n1009=0804C530-G\n1010=073A9510-G\n1011=073A9CC0-G\n1012=07ECD1F0-G\n1013=075BFDC0-G\n1014=0859E2E0-G\n1015=07EA4290-G\n1016=073A8CF0-G\n1017=07ECE4A0-G\n1018=084B2F60-G\n1019=07ECFB60-G\n1020=096473E0-G\n1021=08603140-G\n1022=07EA1E90-G\n1023=08602820-G\n1024=0860DE90-G\n1025=07ED0860-G\n1026=07ED0460-G\n1027=08050A00-G\n1028=07ED1330-G\n1029=073AACA0-G\n1030=075E02F0-G\n1031=09F7B380-G\n1032=073AA3F0-G\n1033=075E2310-G\n1034=075E2180-G\n1035=07E04DC0-G\n1036=085EC2D0-G\n1037=09EF0540-G\n1038=09EF2BB0-G\n1039=09F11CE0-G\n1040=08600780-G\n1041=0965B600-G\n1042=0965D960-G\n1043=08600DC0-G\n1044=08601B60-G\n1045=086019D0-G\n1046=08601770-G\n1047=086015E0-G\n1048=08601380-G\n1049=0846A4D0-G\n1050=08602B40-G\n1051=086029B0-G\n1052=08602640-G\n1053=097246D0-G\n1054=08602DC0-G\n1055=086039B0-G\n1056=0805DAC0-G\n1057=08060740-G\n1058=07F3CAE0-G\n1059=07F3A490-G\n1060=0860B440-G\n\n[08820130-G]\nName=1 juggernaut\n0=1,JUGG\nGroup=-1\n\n[08822830-G]\nName=3 juggernauts\n0=3,JUGG\nGroup=-1\n\n[0A70DB10-G]\nName=1 cyborg reaper\n0=1,REAPER\nGroup=-1\n\n[0A70E6A0-G]\nName=3 cyborg reapers\n0=3,REAPER\nGroup=-1\n\n[0832C3F0-G]\nName=1 amphibious APC, 5 engineers\n0=5,ENGINEER\n1=1,APC\nGroup=-1\n\n[07ECC200-G]\nName=1 subterranean APC, 5 engineers\n0=1,SAPC\n1=5,ENGINEER\nGroup=-1\n\n[08063D20-G]\nName=1 subterranean APC, 3 eng, 2 roc\n0=1,SAPC\n1=3,ENGINEER\n2=2,E3\nGroup=-1\n\n[08063DE0-G]\nName=1 amphibious APC,3 eng, 2 disc\n0=1,APC\n1=3,ENGINEER\n2=2,E2\nGroup=-1\n\n[0804C6C0-G]\nName=1 amphibious APC,1 ghost,4 disc\n0=1,GHOST\n1=1,APC\n2=4,E2\nGroup=-1\n\n[0804C530-G]\nName=1 sub. APC, 1 cyb com, 4 cyborgs\n0=4,CYBORG\n1=1,SAPC\n2=1,CYC2\nGroup=-1\n\n[073A9510-G]\nName=4 attack cycles\n0=4,BIKE\nGroup=-1\n\n[073A9CC0-G]\nName=4 stealth tanks\n0=4,STNK\nGroup=-1\n\n[07ECD1F0-G]\nName=3 wolverines\n0=3,SMECH\nGroup=-1\n\n[075BFDC0-G]\nName=3 hover MLRS\n0=3,HVR\nGroup=-1\n\n[0859E2E0-G]\nName=1 mobile repair vehicle\n0=1,REPAIR\nGroup=-1\n\n[07EA4290-G]\nName=3 artillery\n0=3,ART2\nGroup=-1\n\n[073A8CF0-G]\nName=4 tick tanks\n0=4,TTNK\nGroup=-1\n\n[07ECE4A0-G]\nName=4 devil's tongue\n0=4,SUBTANK\nGroup=-1\n\n[084B2F60-G]\nName=1 amphibious  APC\n0=1,APC\nGroup=-1\n\n[07ECFB60-G]\nName=3 titans\n0=3,MMCH\nGroup=-1\n\n[096473E0-G]\nName=3 disruptors\n0=3,SONIC\nGroup=-1\n\n[08603140-G]\nName=1 ORCA fighters\n0=1,ORCA\nGroup=-1\n\n[07EA1E90-G]\nName=1 ORCA bombers\n0=1,ORCAB\nGroup=-1\n\n[08602820-G]\nName=1 harpies\n0=1,APACHE\nGroup=-1\n\n[0860DE90-G]\nName=1 banshee\n0=1,SCRIN\nGroup=-1\n\n[07ED0860-G]\nName=4 titans\n0=4,MMCH\nGroup=-1\n\n[07ED0460-G]\nName=1 mammoth mk. II\n0=1,HMEC\nGroup=-1\n\n[08050A00-G]\nName=1 sub APC, 1 hijacker, 4 rocket\n0=1,MHIJACK\n1=1,SAPC\n2=4,E3\nGroup=-1\n\n[07ED1330-G]\nName=4 wolverines\n0=4,SMECH\nGroup=-1\n\n[073AACA0-G]\nName=4 attack buggies\n0=4,BGGY\nGroup=-1\n\n[075E02F0-G]\nName=4 disc throwers\n0=4,E2\nGroup=-1\n\n[09F7B380-G]\nName=3 jumpjet infantry\n0=3,JUMPJET\nGroup=-1\n\n[073AA3F0-G]\nName=1 mutant hijacker\n0=1,MHIJACK\nGroup=-1\n\n[075E2310-G]\nName=4 rocket infantry\n0=4,E3\nGroup=-1\n\n[075E2180-G]\nName=4 cyborgs\n0=4,CYBORG\nGroup=-1\n\n[07E04DC0-G]\nName=4 light infantry\n0=4,E1\nGroup=-1\n\n[085EC2D0-G]\nName=1 subterranean APC\n0=1,SAPC\nGroup=-1\n\n[09EF0540-G]\nName=3 engineers\n0=3,ENGINEER\nGroup=-1\n\n[09EF2BB0-G]\nName=1 ghoststalker\n0=1,GHOST\nGroup=-1\n\n[09F11CE0-G]\nName=1 cyborg commando\n0=1,CYC2\nGroup=-1\n\n[08600780-G]\nName=1 titans\n0=1,MMCH\nGroup=-1\n\n[0965B600-G]\nName=1 hover MLRS\n0=1,HVR\nGroup=-1\n\n[0965D960-G]\nName=1 disruptor\n0=1,SONIC\nGroup=-1\n\n[08600DC0-G]\nName=1 disc throwers\n0=1,E2\nGroup=-1\n\n[08601B60-G]\nName=1 jumpjet infantry\n0=1,JUMPJET\nGroup=-1\n\n[086019D0-G]\nName=1 tick tanks\n0=1,TTNK\nGroup=-1\n\n[08601770-G]\nName=1 stealth tanks\n0=1,STNK\nGroup=-1\n\n[086015E0-G]\nName=1 rocket infantry\n0=1,E3\nGroup=-1\n\n[08601380-G]\nName=1 cyborgs\n0=1,CYBORG\nGroup=-1\n\n[0846A4D0-G]\nName=1 attack cycles\n0=1,BIKE\nGroup=-1\n\n[08602B40-G]\nName=1 devil's tongue\n0=1,SUBTANK\nGroup=-1\n\n[086029B0-G]\nName=1 engineers\n0=1,ENGINEER\nGroup=-1\n\n[08602640-G]\nName=1 light infantry\n0=1,E1\nGroup=-1\n\n[097246D0-G]\nName=1 artillery\n0=1,ART2\nGroup=-1\n\n[08602DC0-G]\nName=1 attack buggies\n0=1,BGGY\nGroup=-1\n\n[086039B0-G]\nName=1 wolverines\n0=1,SMECH\nGroup=-1\n\n[0805DAC0-G]\nName=1 amph APC,1 eng, 2 lt. 2 disc\n0=1,ENGINEER\n1=1,APC\n2=2,E2\n3=2,E1\nGroup=-1\n\n[08060740-G]\nName=1 sub APC, 1 eng, 2 rocket, 2 lt\n0=1,ENGINEER\n1=1,SAPC\n2=2,E1\n3=2,E3\nGroup=-1\n\n[07F3CAE0-G]\nName=1 sub. APC, 3 lt, 2 cyborgs\n0=2,CYBORG\n1=3,E1\n2=1,SAPC\nGroup=-1\n\n[07F3A490-G]\nName=1 amphibious APC, 3 lt, 2 disc\n0=1,APC\n1=2,E2\n2=3,E1\nGroup=-1\n\n[0860B440-G]\nName=1 MCV\n0=1,MCV\nGroup=-1\n\n[ScriptTypes]\n1000=07F7C5E0-G\n1001=075AD760-G\n1002=08462780-G\n1003=0786DA60-G\n1004=088DDE00-G\n1005=08463030-G\n1006=075ABE00-G\n1007=07397BE0-G\n1008=07E686F0-G\n1009=085F3E00-G\n1010=085F3980-G\n1011=075A3070-G\n1012=07F7B2A0-G\n1013=07F7D0D0-G\n1014=07F7E3B0-G\n1015=0960AAA0-G\n1016=07F76BE0-G\n1017=07F3DE00-G\n1018=08B50140-G\n\n[07F7C5E0-G]\nName=Base defense attack\n0=0,7\n1=0,1\n\n[075AD760-G]\nName=Construction yard attack\n0=46,131084\n1=49,0\n2=0,1\n\n[08462780-G]\nName=Factories attack\n0=0,6\n1=0,1\n\n[0786DA60-G]\nName=Deployed base defense\n0=9,0\n1=11,10\n\n[088DDE00-G]\nName=Missile silo attack\n0=46,131113\n1=49,0\n2=0,1\n\n[08463030-G]\nName=Power facilities attack\n0=0,9\n1=0,1\n\n[075ABE00-G]\nName=Tiberium refinery attack\n0=46,131073\n1=49,0\n2=0,1\n\n[07397BE0-G]\nName=Upgrade center attack\n0=46,131076\n1=49,0\n2=0,1\n\n[07E686F0-G]\nName=Base defense\n0=11,10\n\n[085F3E00-G]\nName=APC/engineer attack\n0=14,0\n1=43,0\n2=47,131084\n3=49,0\n4=8,2\n5=11,14\n\n[085F3980-G]\nName=APC/eng. steal money\n0=14,0\n1=43,0\n2=47,131073\n3=49,0\n4=8,2\n5=46,131073\n6=46,131074\n7=11,14\n\n[075A3070-G]\nName=APC/commando attack\n0=14,0\n1=47,131084\n2=49,0\n3=8,2\n4=0,2\n5=0,1\n\n[07F7B2A0-G]\nName=Harvester attack\n0=0,3\n1=0,1\n\n[07F7D0D0-G]\nName=Aerial base attack\n0=0,2\n1=0,1\n\n[07F7E3B0-G]\nName=Vehicle attack\n0=0,5\n1=0,1\n\n[0960AAA0-G]\nName=APC/thief steal vehicles\n0=14,0\n1=47,1\n2=49,0\n3=8,2\n4=0,5\n\n[07F76BE0-G]\nName=Infantry attack\n0=0,4\n1=0,1\n\n[07F3DE00-G]\nName=APC/infantry attack\n0=14,0\n1=43,0\n2=47,12\n3=49,0\n4=8,2\n5=0,1\n\n[08B50140-G]\nName=Replace MCV\n0=9,0\n\n[TeamTypes]\n1000=08820200-G\n1001=08824090-G\n1002=08822750-G\n1003=08D1B060-G\n1004=09E4A280-G\n1005=09E4A4F0-G\n1006=09E4FF40-G\n1007=09E4FE60-G\n1008=09E4FD80-G\n1009=041A7830-G\n1010=041A7750-G\n1011=041A7670-G\n1012=041A7590-G\n1013=041A74B0-G\n1014=07CC69F0-G\n1015=07CC6910-G\n1016=0A70DBE0-G\n1017=0A70E770-G\n1018=0A70E140-G\n1019=0A70E060-G\n1020=0A70FC10-G\n1021=0A70FB30-G\n1022=0A70F720-G\n1023=0A70F640-G\n1024=095E1D80-G\n1025=095E1CA0-G\n1026=08823D20-G\n1027=08823C40-G\n1028=08822150-G\n1029=095E1920-G\n1030=08823930-G\n1031=08823850-G\n\n[08820200-G]\nName=E_GDI base defense attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=08820130-G\n\n[08824090-G]\nName=H_GDI base defense attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=08822830-G\n\n[08822750-G]\nName=E_GDI con. yard attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08820130-G\n\n[08D1B060-G]\nName=H_GDI con. yard attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=08822830-G\n\n[09E4A280-G]\nName=E_GDI factories attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08820130-G\n\n[09E4A4F0-G]\nName=H_GDI factories attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=08822830-G\n\n[09E4FF40-G]\nName=E_GDI juggernaut pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=08820130-G\n\n[09E4FE60-G]\nName=H_GDI juggernaut pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=0786DA60-G\nTaskForce=08822830-G\n\n[09E4FD80-G]\nName=E_GDI missile silo attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08820130-G\n\n[041A7830-G]\nName=H_GDI missile silo attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=08822830-G\n\n[041A7750-G]\nName=E_GDI power facilities attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08820130-G\n\n[041A7670-G]\nName=H_GDI power facilities attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=08822830-G\n\n[041A7590-G]\nName=E_GDI tib. refinery attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08820130-G\n\n[041A74B0-G]\nName=H_GDI tib. refinery attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=08822830-G\n\n[07CC69F0-G]\nName=E_GDI upgrade center attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08820130-G\n\n[07CC6910-G]\nName=H_GDI upgrade center attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=GDI\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=08822830-G\n\n[0A70DBE0-G]\nName=E_Nod cyborg reaper pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0A70DB10-G\n\n[0A70E770-G]\nName=H_Nod cyborg reaper pool\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=yes\nLooseRecruit=no\nAggressive=no\nSuicide=no\nPriority=4\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=yes\nOnlyTargetHouseEnemy=yes\nScript=07E686F0-G\nTaskForce=0A70E6A0-G\n\n[0A70E140-G]\nName=E_Nod con. yard attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0A70DB10-G\n\n[0A70E060-G]\nName=H_Nod con. yard attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075AD760-G\nTaskForce=0A70E6A0-G\n\n[0A70FC10-G]\nName=E_Nod factories attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0A70DB10-G\n\n[0A70FB30-G]\nName=H_Nod factories attack 7\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08462780-G\nTaskForce=0A70E6A0-G\n\n[0A70F720-G]\nName=E_Nod missile silo attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0A70DB10-G\n\n[0A70F640-G]\nName=H_Nod missile silo attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=088DDE00-G\nTaskForce=0A70E6A0-G\n\n[095E1D80-G]\nName=E_Nod power facilities attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0A70DB10-G\n\n[095E1CA0-G]\nName=H_Nod power facilities attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=12\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=08463030-G\nTaskForce=0A70E6A0-G\n\n[08823D20-G]\nName=E_Nod tib. refinery attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0A70DB10-G\n\n[08823C40-G]\nName=H_Nod tib. refinery attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=075ABE00-G\nTaskForce=0A70E6A0-G\n\n[08822150-G]\nName=E_Nod upgrade center attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0A70DB10-G\n\n[095E1920-G]\nName=H_Nod upgrade center attack 5\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07397BE0-G\nTaskForce=0A70E6A0-G\n\n[08823930-G]\nName=E_Nod base defense attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=no\nSuicide=yes\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=0A70DB10-G\n\n[08823850-G]\nName=H_Nod base defense attack 3\nVeteranLevel=1\nLoadable=no\nFull=no\nAnnoyance=no\nGuardSlower=no\nHouse=Nod\nRecruiter=no\nAutocreate=no\nPrebuild=no\nReinforce=no\nDroppod=no\nWhiner=no\nLooseRecruit=no\nAggressive=yes\nSuicide=no\nPriority=8\nMax=2\nTechLevel=0\nGroup=-1\nOnTransOnly=no\nAvoidThreats=no\nIonImmune=no\nTransportsReturnOnUnload=no\nAreTeamMembersRecruitable=yes\nIsBaseDefense=no\nOnlyTargetHouseEnemy=yes\nScript=07F7C5E0-G\nTaskForce=0A70E6A0-G\n\n[AITriggerTypes]\n07CC6F10-G=E_GDI juggernaut pool,09E4FF40-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,09E4FF40-G,1,0,0\n07CC6E00-G=M_GDI juggernaut pool,09E4FE60-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n07CC6CF0-G=H_GDI juggernaut pool,09E4FE60-G,<all>,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,09E4FE60-G,0,0,1\n07CC6580-G=E_GDI base defense attack 3,08820200-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,08820200-G,1,0,0\n07CC6470-G=M_GDI base defense attack 3,08824090-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n07CC6360-G=H_GDI base defense attack 3,08824090-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08824090-G,0,0,1\n07CC6250-G=E_GDI con. yard attack 5,08822750-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,08822750-G,1,0,0\n07CC6140-G=M_GDI con. yard attack 5,08D1B060-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n07CC6030-G=H_GDI con. yard attack 5,08D1B060-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,08D1B060-G,0,0,1\n07CC7F10-G=E_GDI factories attack 7,09E4A280-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,09E4A280-G,1,0,0\n07CC7E00-G=M_GDI factories attack 7,09E4A4F0-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,<none>,0,1,0\n07CC7CF0-G=H_GDI factories attack 7,09E4A4F0-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,09E4A4F0-G,0,0,1\n07CC7BE0-G=E_GDI missile silo attack 5,09E4FD80-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,09E4FD80-G,1,0,0\n07CC7AD0-G=M_GDI missile silo attack 5,041A7830-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n07CC79C0-G=H_GDI missile silo attack 5,041A7830-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,041A7830-G,0,0,1\n07CC78B0-G=E_GDI power facilities attack 5,041A7750-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041A7750-G,1,0,0\n07CC77A0-G=M_GDI power facilities attack 5,041A7670-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,<none>,0,1,0\n07CC7690-G=H_GDI power facilities attack 5,041A7670-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,041A7670-G,0,0,1\n041CFAE0-G=E_GDI tib. refinery attack 5,041A7590-G,<all>,6,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,041A7590-G,1,0,0\n07CC6BE0-G=M_GDI tib. refinery attack 5,041A74B0-G,<all>,6,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n07CC6AD0-G=H_GDI tib. refinery attack 5,041A74B0-G,<all>,6,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,041A74B0-G,0,0,1\n07CC6800-G=E_GDI upgrade center attack 5,07CC69F0-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,07CC69F0-G,1,0,0\n07CC66F0-G=M_GDI upgrade center attack 5,07CC6910-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,<none>,0,1,0\n08850F10-G=H_GDI upgrade center attack 5,07CC6910-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,07CC6910-G,0,0,1\n0A70E440-G=E_Nod cyborg reaper pool,0A70DBE0-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A70DBE0-G,1,0,0\n0A70E330-G=M_Nod cyborg reaper pool,0A70E770-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n0A70E220-G=H_Nod cyborg reaper pool,0A70E770-G,<all>,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0A70E770-G,0,0,1\n0A70FF10-G=E_Nod con. yard attack 5,0A70E140-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,0A70E140-G,1,0,0\n0A70FE00-G=M_Nod con. yard attack 5,0A70E060-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n0A70FCF0-G=H_Nod con. yard attack 5,0A70E060-G,<all>,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0A70E060-G,0,0,1\n0A70FA20-G=E_Nod factories attack 7,0A70FC10-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A70FC10-G,1,0,0\n0A70F910-G=M_Nod factories attack 7,0A70FB30-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n0A70F800-G=H_Nod factories attack 7,0A70FB30-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0A70FB30-G,0,0,1\n0A70F530-G=E_Nod missile silo attack 5,0A70F720-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0A70F720-G,1,0,0\n0A70F420-G=M_Nod missile silo attack 5,0A70F640-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n0A70F310-G=H_Nod missile silo attack 5,0A70F640-G,<all>,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,0A70F640-G,0,0,1\n0A70F040-G=E_Nod power facilities attack 5,095E1D80-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,095E1D80-G,1,0,0\n07CDB770-G=M_Nod power facilities attack 5,095E1CA0-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,<none>,0,1,0\n08823F10-G=H_Nod power facilities attack 5,095E1CA0-G,<all>,6,3,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,095E1CA0-G,0,0,1\n08821F10-G=E_Nod upgrade center attack 5,08822150-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,08822150-G,1,0,0\n08821E00-G=M_Nod upgrade center attack 5,095E1920-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n08821CF0-G=H_Nod upgrade center attack 5,095E1920-G,<all>,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,095E1920-G,0,0,1\n08823740-G=E_Nod base defense attack 3,08823930-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08823930-G,1,0,0\n08823630-G=M_Nod base defense attack 3,08823850-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,<none>,0,1,0\n08823520-G=H_Nod base defense attack 3,08823850-G,<all>,6,-1,<none>,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,08823850-G,0,0,1\n08860D10-G=E_Nod tib. refinery attack 5,08823D20-G,<all>,6,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,08823D20-G,1,0,0\n08860C00-G=M_Nod tib. refinery attack 5,08823C40-G,<all>,6,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,<none>,0,1,0\n08860AF0-G=H_Nod tib. refinery attack 5,08823C40-G,<all>,6,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,08823C40-G,0,0,1\n\n[Digest]\n1=VIYs66hN8BSm544VkgK0QVZgCtw=\n"
  },
  {
    "path": "DXMainClient/Resources/INI/art.ini",
    "content": "; ART.INI\n; This control file provides the information necessary to handle the\n; artwork in Tiberian Sun. The information usually covers sprite\n; animation and related characteristics. There is one section\n; for each sprite data file. The sprite section names are unique\n; within the system and are referred to by name in the RULES.INI\n; control file.\n\n; *** Game Object Art ***\n; Cameo = image to use if this object happens to appear in the sidebar (def=none)\n; Voxel = Is this a voxel image (def=no)?\n; Remapable = Can this object be remapped to owner's color (def=no)?\n; Normalized = If its animation be regulated to appear constant speed (def=no)?\n; Theater = Does it have theater specific imagery (def=no)?\n; NewTheater = Does it have theater specific name (def=no)? This changes the second character of the name dependant on theater.\n; RotCount = number of rotation stages [old system only] (def=32)?\n; ShadowIndex = index of voxel piece to use for the shadow (def=0) [only needed for voxel hierarchies]\n; TurretOffset = turret center offset along body centerline (def=0)\n; FireAngle = default angle to raise barrel above direct line to targe [in degrees] (def=10)\n; BarrelLength = length of barrel expressed in 1.5 inch increments (def = 0 i.e., no independant barrel equipped)\n; BarrelOffset = barrel pivot point [distance from center-base along X (horz) axis and Y (vert) axis]\n; PrimaryFireFLH = lepton offset [Forward,Lateral,Height from turret center] for bullet start position (def=0,0,0)\n; SecondaryFireFLH = alternate weapon offset for bullet start position (def=0,0,0)\n;  <<< applies only to artwork used for infantry >>>\n;    Sequence = infantry animation sequence name [required]\n;    Crawls = Does the infantry have crawling animation [else it is running] (def=yes)?\n;    FireUp = frame of projectile launch when firing standing [required if it has firing animation] (def=0)\n;  <<< applies only to vehicles >>>\n;    VisibleLoad = Does the unit have duplicate shape set for loaded with ammo state (def=no)?\n;    UseTurretShadow = use the turret of the object to cast shadow (def=no)?\n;  <<< applies only to building types >>>\n;    Foundation = the size of the building [width x height] (def=1x1)\n;    Height = height of the building [in levels]\n;    PrimaryFirePixelOffset = Pixel offset to apply to building when firing a bullet. Used when the building has no turret and so the offset is fixed.\n;    SecondaryFirePixelOffset = Pixel offset to apply to building when firing a bullet. Used when the building has no turret and so the offset is fixed.\n;    SimpleDamage = Does building have simple damage imagery (def=yes)?\n;    Buildup = graphic image to use when construction occurs (def=none)\n;    AuxAnim = Anim to use for overlaying animation states.\n;    AltImage = Is there an alternate image [frame #2] to use when viewed by enemy player (def=no)?\n;    ChargeAnim = Does this building have Tesla-coil like charge up anim (def=no)?\n;    SiloDamage = Is damage image based on base Tiberium storage level (def=no)?\n;    Flat = Is building flat on the ground [helps with drawing logic to know this] (def=no)?\n;    Recoilless = Does the building NOT have recoil anim even though it might have a turret (def=no)?\n;    ToOverlay = when placed down, actually convert into this overlay type (def=none)\n;    DamageLevels = how many levels of damage it can take [for walls only]\n;    PowerUp1Anim = The animation to add to this building when powered up by one level\n;    PowerUp1AnimDamaged = Damaged version of The animation to add to this building when powered up by one level\n;    PowerUp1LocX = The x offset from the buildings draw position for this powerup animation\n;    PowerUp1LocY = The x offset from the buildings draw position for this powerup animation\n;    PowerUp1LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building.\n;    PowerUp1YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects\n;    PowerUp2Anim = The animation to add to this building (in addition to the level 1 power up) when powered up by two levels\n;    PowerUp2AnimDamaged = Damaged version of The animation to add to this building when powered up by two level\n;    PowerUp2LocX = The x offset from the buildings draw position for this powerup animation\n;    PowerUp2LocY = The x offset from the buildings draw position for this powerup animation\n;    PowerUp2LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building.\n;    PowerUp2YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects\n;    PowerUp3Anim = The animation to add to this building (in addition to the level 1&2 power ups) when powered up by three levels\n;    PowerUp3AnimDamaged = Damaged version of The animation to add to this building when powered up by three level\n;    PowerUp3LocX = The x offset from the buildings draw position for this powerup animation\n;    PowerUp3LocY = The x offset from the buildings draw position for this powerup animation\n;    PowerUp3LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building.\n;    PowerUp3YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects\n;    ActiveAnim = Animation to use for building active animation\n;    ActiveAnimDamaged = Animation to use for building active animation when damaged\n;    ActiveAnimX = X offset from building position for active animation\n;    ActiveAnimY = Y offset from building position for active animation\n;    ActiveAnimYSort = Adjustment to position to use when sorting the layer prior to rendering.\n;    ActiveAnimZAdjust = Adjustment to normal Z when rendering this animation over the building.\n;    ActiveAnimPowered = Does the animation require that the building has power (def=yes)\n;    ActiveAnimTwo = Animation to use for building active animation\n;    ActiveAnimTwoDamaged = Animation to use for building active animation when damaged\n;    ActiveAnimTwoX = X offset from building position for active animation\n;    ActiveAnimTwoY = Y offset from building position for active animation\n;    ActiveAnimTwoYSort = Adjustment to position to use when sorting the layer prior to rendering.\n;    ActiveAnimTwoZAdjust = Adjustment to normal Z when rendering this animation over the building.\n;    ActiveAnimTwoPowered = Does the animation require that the building has power (def=yes)\n;    ActiveAnimThree = Animation to use for building active animation\n;    ActiveAnimThreeDamaged = Animation to use for building active animation when damaged\n;    ActiveAnimThreeX = X offset from building position for active animation\n;    ActiveAnimThreeY = Y offset from building position for active animation\n;    ActiveAnimThreeYSort = Adjustment to position to use when sorting the layer prior to rendering.\n;    ActiveAnimThreeZAdjust = Adjustment to normal Z when rendering this animation over the building.\n;    ActiveAnimThreePowered = Does the animation require that the building has power (def=yes)\n;    TerrainPalette = Draw this in the terrain palette not the building palette. (def=no)\n;  <<< applies on to vessels >>>\n;    Rotates = Does the vessel rotate [old system only] (def=yes)?\n;  <<< applies only to aircraft >>>\n;    Rotors = Does this aicraft have an attached rotor animation (def=no)?\n;    CustomRotor = Does it have custom rotor shapes according to facing (def=no)?\n\n[JUMPJET]\nCameo=JJETICON\nSequence=JumpjetSequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=100,0,120\n\n[CYC2]\nCameo=CYBCICON\nSequence=CyborgSequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=100,-50,130\n\n[CHAMSPY]\nCameo=CHAMICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[E1]\nCameo=E1ICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=80,0,85\n\n[E2]\nCameo=E2ICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=6\nPrimaryFireFLH=60,0,100\nFireProne=4  ; FireProne can be used to delay firing to sync with the firing animation\n\n[E3]\nCameo=E4ICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=100,-25,135\n\n;[WEEDGUY]\n;Cameo=WEATICON\n;Sequence=WeedSequence\n;Crawls=yes\n;Remapable=yes\n;FireUp=2\n\n[MEDIC]\nCameo=MEDIICON\nSequence=MedicSequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=0,0,100\n\n[GHOST]\nSequence=E1Sequence\nCameo=GOSTICON\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=100,0,100\n\n[MHIJACK]\nSequence=E1Sequence\nCameo=CHAMICON\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[SLAV]\nSequence=E1Sequence\nCameo=WEATICON\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n; Nod Elite Cadre Soldier\n[ELCAD]\nSequence=E1Sequence\nCameo=WEATICON\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[CYBORG]\nCameo=CYBIICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=70,-30,120\nFireProne=4  ; FireProne can be used to delay firing to sync with the firing animation/burst\n\n[MUTANT]\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[UMAGON]\nSequence=E1Sequence\nCameo=UMAGICON\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=100,0,100\n\n[DOGGIE]\nSequence=DoggieSequence\nCrawls=yes\nRemapable=yes\nFireUp=2\nPrimaryFireFLH=0,0,100\n\n[MWMN]\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[OXANNA]\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[TRATOS]\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[MUTANT3]\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[ENGINEER]\nCameo=ENGNICON\nSequence=E1Sequence\nCrawls=yes\nRemapable=yes\nFireUp=2\n\n[CIV1]\nSequence=E1Sequence\nCrawls=no\nFireUp=2\n\n[CIV2]\nSequence=E1Sequence\nCrawls=no\nFireUp=2\n\n[CIV3]\nSequence=E1Sequence\nCrawls=no\nFireUp=2\n\n; Vehicle artwork follows\n\n[TRUCKA]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=40,32,96\nSecondaryFireFLH=-32,80,120\nPBarrelLength=192\n\n[TRUCKB]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=40,32,96\nSecondaryFireFLH=-32,80,120\nPBarrelLength=192\n\n[4TNK]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=40,32,96\nSecondaryFireFLH=-32,80,120\nPBarrelLength=192\n\n[ART2]\nCameo=ARTYICON\nRemapable=yes\nTurretOffset=-56\nPBarrelLength=224\nVoxel=yes\nPrimaryFireFLH=0,0,64\n\n[WEED]\nCameo=WEEDICON\nVoxel=yes\nRemapable=yes\n\n[HARV]\nCameo=HARVICON\nVoxel=yes\nRemapable=yes\n\n[HORV]\nVoxel=yes\nRemapable=yes\n\n[REPAIR]\nCameo=RBOTICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=110,0,105\n\n[LPST]\nCameo=LPSTICON\nVoxel=yes\nRemapable=yes\n\n[ICBM]\nVoxel=yes\nRemapable=yes\n\n[MCV]\nCameo=MCVICON\nRemapable=yes\nVoxel=yes\n\n[HVR]\nCameo=HOVRICON\nVoxel=yes\nTurretOffset=-64\nRemapable=yes\nPrimaryFireFLH=64,32,128\n\n[APC]\nCameo=APCICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,48,48\n\n[MMCH]\nVoxel=no\nRemapable=yes\nCameo=MMCHICON\nPrimaryFireFLH=0,-50,100\nPBarrelLength=250\nSBarrelLength=250\nTurretOffset=0\nWalkFrames=15\n\n[HMEC]\nCameo=HMECICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=80,100,158\nSecondaryFireFLH=-60,100,158\nUseTurretShadow=yes\nShadowIndex=12\n\n[GGHUNT]\nVoxel=no\nRemapable=yes\nWalkFrames=8\n\n[SMECH]\nVoxel=no\nRemapable=yes\nCameo=SMCHICON\nPrimaryFireFLH=0,48,48\nWalkFrames=12\nFiringFrames=4\n\n[TTNK]\nCameo=TICKICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,100\nPBarrelLength=136\nTurretOffset=64\n\n[BIKE]\nCameo=CYCLICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,48,48\n\n[BGGY]\nCameo=BGGYICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,100\n\n[SAPC]\nCameo=SAPCICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,48\n\n[STNK]\nCameo=STNKICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=40,0,104\n\n[SUBTANK]\nCameo=SUBTICON\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=128,0,40\n\n[SONIC]\nCameo=SONIICON\nTurretOffset=-64\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,150\n\n[BUS]\nVoxel=yes\n\n[MONOCAR]\nVoxel=yes\n\n[CARGOCAR]\nVoxel=yes\n\n[MONOENG]\nVoxel=yes\n\n[PICK]\nVoxel=yes\n\n[CAR]\nVoxel=yes\n\n[WINI]\nVoxel=yes\n\n[1TNK]\nVoxel=yes\nRemapable=yes\n\n[2TNK]\nVoxel=yes\nRemapable=yes\n\n[3TNK]\nVoxel=yes\nRemapable=yes\n\n[ARTY]\nVoxel=yes\nRemapable=yes\n\n[HELI]\nVoxel=yes\nRemapable=yes\n\n[HIND]\nVoxel=yes\nRemapable=yes\n\n[JEEP]\nVoxel=yes\nRemapable=yes\n\n[M113]\nVoxel=yes\nRemapable=yes\n\n[MLRS]\nVoxel=yes\nRemapable=yes\n\n[MNLY]\nVoxel=yes\nRemapable=yes\n\n[MRJ]\nVoxel=yes\nRemapable=yes\n\n[TRAN]\nVoxel=yes\nRemapable=yes\n\n[TRUCK2]\nVoxel=yes\nRemapable=yes\n\n[TRUK]\nVoxel=yes\nRemapable=yes\n\n[UTNK]\nVoxel=yes\nRemapable=yes\n\n; Aircraft artwork follows\n\n[ORCAB]\nCameo=OBMBICON\nVoxel=yes\nPrimaryFireFLH=0,32,0\n\n[ORCA]\nCameo=ORCAICON\nVoxel=yes\nPrimaryFireFLH=0,32,0\n\n[ORCATRAN]\nCameo=CRRYICON\nVoxel=yes\n\n[TRNSPORT]\nCameo=OTRNICON\nVoxel=yes\n\n[SCRIN]\nCameo=PROICON\nVoxel=yes\nPrimaryFireFLH=0,32,0\n\n[APACHE]\nCameo=APCHICON\nVoxel=yes\nPrimaryFireFLH=0,32,0\n\n[DPOD]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,0\n\n[DSHP]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=0,0,0\n\n; firestorm addition\n[FTNK]\nVoxel=yes\nRemapable=yes\nPrimaryFireFLH=175,30,0\n\n; Building artwork follows\n\n[GATECH]\nRemapable=yes\nNormalized=yes\nCameo=TECHICON\nHeight=1\nFoundation=3x2\nBuildup=GATECHMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GATECH_A\nActiveAnimZAdjust=-100\nActiveAnimDamaged=GATECH_AD\n\n[GAWEAP]\nRemapable=yes\nCameo=WEAPICON\nFoundation=4x3\nHeight=2\nNormalZAdjust=-10\nAnimActive=0,1,0\nBuildup=GAWEAPMK\nDemandLoadBuildup=true\nFreeBuildup=true\nDeployingAnim=GAWEAP_2\nDoorAnim=GAWEAP_D\nDoorStages=9\nUnderDoorAnim=GAWEAP_1\nNewTheater=yes\nBibShape=GAWEAPBB\nActiveAnim=GAWEAP_A\nActiveAnimZAdjust=-119\nActiveAnimTwo=GAWEAP_B\nActiveAnimTwoZAdjust=-119\nActiveAnimThree=GAWEAP_C\nActiveAnimThreeZAdjust=-119\n\n[NAWEAP]\nRemapable=yes\nCameo=NWEPICON\nFoundation=4x3\nHeight=2\nAnimActive=0,1,0\nBuildup=NAWEAPMK\nDemandLoadBuildup=true\nFreeBuildup=true\nDeployingAnim=NAWEAP_2\nDoorAnim=NAWEAP_B\nDoorStages=10\nDamagedDoor=yes\nUnderDoorAnim=NAWEAP_1\nNewTheater=yes\nBibShape=NAWEAPBB\nProductionAnim=NAWEAP_A\nProductionAnimX=0\nProductionAnimY=0\nProductionAnimYSort=0\nProductionAnimZAdjust=-119\nActiveAnim=NAWEAP_A\nActiveAnimDamaged=NAWEAP_AD\nActiveAnimZAdjust=-119\n\n[GACNST]\nRemapable=yes\nFoundation=3x3\nHeight=1.5\nAnimActive=0,26,3\nBuildup=GACNSTMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GACNST_A\nActiveAnimDamaged=GACNST_AD\nActiveAnimZAdjust=-77\nActiveAnimTwo=GACNST_C\nActiveAnimTwoDamaged=GACNST_CD\nActiveAnimTwoZAdjust=-77\nActiveAnimThree=GACNST_B\nActiveAnimThreeZAdjust=-27\nPreProductionAnim=GACNST_B\nPreProductionAnimZAdjust=-15\nProductionAnim=GACNST_D\nProductionAnimZAdjust=-25\n\n[NAREFN]\nRemapable=yes\nCameo=REFICON\nFoundation=4x3\nHeight=2\nZShapePointMove= 24, -12\nBuildup=NAREFNMK\nDemandLoadBuildup=true\nFreeBuildup=true\nBibShape=NAREFNBB\nNewTheater=yes\nActiveAnim=NAREFN_C\nActiveAnimZAdjust=-100\nActiveAnimTwo=NAREFN_B\nActiveAnimTwoZAdjust=-100\nActiveAnimTwoPowered=no\nPreProductionAnim=NAREFN_A\nProductionAnim=NAREFN_AR\nPreProductionAnimX=-2\nPreProductionAnimY=2\nPreProductionAnimZAdjust=-100\nProductionAnimX=-2\nProductionAnimY=2\nProductionAnimZAdjust=-100\n\n[PROC]\nImage=NAREFN\nRemapable=yes\nCameo=REFICON\nFoundation=4x3\nHeight=2\nZShapePointMove= 24, -12\nBuildup=NAREFNMK\nDemandLoadBuildup=true\nFreeBuildup=true\nBibShape=NAREFNBB\nNewTheater=yes\nActiveAnim=NAREFN_C\nActiveAnimZAdjust=-100\nActiveAnimTwo=NAREFN_B\nActiveAnimTwoZAdjust=-100\nActiveAnimTwoPowered=no\nPreProductionAnim=NAREFN_A\nProductionAnim=NAREFN_AR\nPreProductionAnimX=-2\nPreProductionAnimY=2\nPreProductionAnimZAdjust=-100\nProductionAnimX=-2\nProductionAnimY=2\nProductionAnimZAdjust=-100\n\n[GASILO]\nRemapable=yes\nCameo=SILOICON\nFoundation=2x2\nBuildup=GASILOMK\nDemandLoadBuildup=true\nFreeBuildup=true\nSiloDamage=yes\nNewTheater=yes\nActiveAnim=GASILO_B\nActiveAnimDamaged=GASILO_BD\nActiveAnimZAdjust=-100\nSpecialAnim=GASILO_A\nSpecialAnimDamaged=GASILO_AD\nSpecialAnimZAdjust=-32\n\n\n[GAHPAD]\nRemapable=yes\nCameo=HELIICON\nFoundation=2x2\nHeight=1\nFlat=yes\nBuildup=GAHPADMK\nBibShape=GAHPADBB\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GAHPAD_A\nActiveAnimY=-12\nActiveAnimZAdjust=-32\nActiveAnimDamaged=GAHPAD_AD\n\n[NAHPAD]\nRemapable=yes\nCameo=NHPDICON\nFoundation=2x2\nHeight=1.5\nFlat=yes\nBuildup=NAHPADMK\nBibShape=NAHPADBB\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAHPAD_A\nActiveAnimZAdjust=-32\nActiveAnimDamaged=NAHPAD_AD\n\n[NARADR]\nRemapable=yes\nNormalized=yes\nHeight=3\nCameo=NRADICON\nFoundation=2x2\nBuildup=NARADRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NARADR_A\nActiveAnimDamaged=NARADR_AD\nActiveAnimZAdjust=-60\nActiveAnimYSort=100\n\n[GAPLUG]\nRemapable=yes\nNormalized=yes\nCameo=PLUGICON\nFoundation=2x3\nHeight=2\nBuildup=GAPLUGMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnimZAdjust=-100\nActiveAnimTwoZAdjust=-125\nActiveAnim=GAPLUG_A\nActiveAnimPowered=yes\nActiveAnimTwo=GAPLUG_B\nActiveAnimTwoDamaged=GAPLUG_BD\nActiveAnimTwoPowered=no\nActiveAnimTwoPoweredLight=yes\nActiveAnimThree=GAPLUG_C\nActiveAnimThreeZAdjust=-60\nActiveAnimThreePowered=no\nActiveAnimThreePoweredLight=yes\nPowerUp1LocXX=0\nPowerUp1LocYY=0\nPowerUp1LocZZ=-30\nPowerUp1YSort=-5\nPowerUp2LocXX=-24\nPowerUp2LocYY=-12\nPowerUp2LocZZ=-42\nPowerUp2YSort=50\n\n[GARADR]\nRemapable=yes\nNormalized=yes\nCameo=RADRICON\nFoundation=2x2\nHeight=4\nBuildup=GARADRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GARADR_A\nActiveAnimDamaged=GARADR_AD\nActiveAnimZAdjust=-60\nActiveAnimPowered=yes\n\n[NASTLH]\nCameo=CLCKICON\nRemapable=yes\nNormalized=yes\nFoundation=3x2\nBuildup=NASTLHMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NASTLH_A\nActiveAnimDamaged=NASTLH_AD\nActiveAnimZAdjust=-40\n\n[GAPOWR]\nNormalized=yes\nRemapable=yes\nCameo=POWRICON\nFoundation=2x2\nBuildup=GAPOWRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nHeight=2\nPowerUp1Anim=GAPOWR_B\nPowerUp1LocXX=-24\nPowerUp1LocYY=13\nPowerUp1LocZZ=-17\nPowerUp1YSort=-5\n;PowerUp2Anim=GAPOWR_B\n;PowerUp2LocXX=0\n;PowerUp2LocYY=0\n;PowerUp2LocZZ=-32\n;PowerUp2YSort=-5\nPowerUp2Anim=GAPOWR_B\nPowerUp2LocXX=-48\nPowerUp2LocYY=0\nPowerUp2LocZZ=-32\nPowerUp2YSort=-5\nActiveAnim=GAPOWR_A\nActiveAnimDamaged=GAPOWR_AD\nActiveAnimZAdjust=-100\nActiveAnimTwo=GAPOWR_B\nActiveAnimTwoZAdjust=-32\nActiveAnimTwoYSort=-5\n\n[NAPOWR]\nNormalized=yes\nRemapable=yes\nCameo=NPWRICON\nFoundation=2x2\nHeight=2\nBuildup=NAPOWRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAPOWR_A\nActiveAnimDamaged=NAPOWR_AD\nActiveAnimZAdjust=-55\n\n[NAAPWR]\nNormalized=yes\nRemapable=yes\nCameo=APWRICON\nFoundation=2x3\nHeight=2\nBuildup=NAAPWRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAAPWR_A\nActiveAnimDamaged=NAAPWR_AD\nActiveAnimZAdjust=-55\n\n[CAHOSP]\nNormalized=yes\nRemapable=no\nFoundation=3x4\nHeight=2\nBuildup=CAHOSP\nNewTheater=yes\nActiveAnim=CAHOSP_A\nActiveAnimZAdjust=-100\nDemandLoad=true\n\n[CAARMR]\nNormalized=yes\nRemapable=no\nFoundation=4x4\nHeight=1\nBuildup=CAARMR\nNewTheater=yes\nActiveAnim=CAARMR_A\nActiveAnimZAdjust=-100\nActiveAnimPowered=no\nDemandLoad=true\n\n[CAPYR01]\nNormalized=yes\nRemapable=no\nFoundation=2x2\nHeight=2\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CAPYR02]\nNormalized=yes\nRemapable=no\nFoundation=4x4\nHeight=4\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CAPYR03]\nNormalized=yes\nRemapable=no\nFoundation=4x4\nHeight=4\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CACRSH01]\nNormalized=yes\nRemapable=no\nFoundation=1x1\nHeight=1\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CACRSH02]\nNormalized=yes\nRemapable=no\nFoundation=1x1\nHeight=1\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CACRSH03]\nNormalized=yes\nRemapable=no\nFoundation=1x1\nHeight=1\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CACRSH04]\nNormalized=yes\nRemapable=no\nFoundation=1x1\nHeight=1\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CACRSH05]\nNormalized=yes\nRemapable=no\nFoundation=1x1\nHeight=1\nNewTheater=yes\nTerrainPalette=yes\nExtraDamageStage=no\nDemandLoad=true\n\n[CAARAY]\nCameo=\nNormalized=yes\nRemapable=no\nFoundation=2x2\nBuildup=\nHeight=3\nNewTheater=yes\nActiveAnim=CAARAY_A\nActiveAnimZAdjust=-100\nActiveAnimTwo=CAARAY_B\nActiveAnimTwoZAdjust=-100\nActiveAnimThree=CAARAY_C\nActiveAnimThreeDamaged=CAARAY_CD\nActiveAnimThreeZAdjust=-100\nActiveAnimFour=CAARAY_D\nActiveAnimFourDamaged=CAARAY_DD\nActiveAnimFourZAdjust=-100\nDemandLoad=true\n\n\n[GASPOT]\nCameo=SPOTICON\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=GASPOTMK\nDemandLoadBuildup=true\nHeight=3\nNewTheater=yes\nActiveAnim=GASPOT_A\nActiveAnimDamaged=GASPOT_AD\nActiveAnimZAdjust=-100\n\n[GADPSA]\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=GADPSAMK\nHeight=2\nNewTheater=yes\nActiveAnim=GADPSA_A\nActiveAnimZAdjust=-100\nExtraLight=-100\n\n[GAICBM]\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=GAICBMMK\nDemandLoadBuildup=true\nFreeBuildup=true\nHeight=2\nNewTheater=yes\nActiveAnim=GAICBM_A\nActiveAnimZAdjust=-100\nExtraLight=-100\n\n[GATICK]\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=GATICKMK\nHeight=1\nNewTheater=yes\nExtraLight=-100\nPrimaryFireFLH=48,0,64\nPBarrelLength=136\n\n[GAARTY]\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=GAARTYMK\nHeight=1\nNewTheater=yes\nExtraLight=350\nPBarrelLength=224\nPrimaryFireFLH=0,0,64\nTurretNotExportedOnGround=yes\n\n[NATECH]\nRemapable=yes\nNormalized=yes\nCameo=NTCHICON\nFoundation=2x2\nBuildup=NATECHMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NATECH_A\nActiveAnimZAdjust=-100\n\n[NAHAND]\nRemapable=yes\nNormalized=yes\nCameo=HANDICON\nFoundation=3x2\nBuildup=NAHANDMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAHAND_A\nActiveAnimZAdjust=-100\nActiveAnimTwo=NAHAND_B\nActiveAnimTwoDamaged=NAHAND_BD\nActiveAnimTwoZAdjust=-100\n\n[GAPILE]\nRemapable=yes\nNormalized=yes\nCameo=BRRKICON\nFoundation=2x2\nBuildup=GAPILEMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GAPILE_C\nActiveAnimDamaged=GAPILE_CD\nActiveAnimZAdjust=-39\nActiveAnimPowered=no\nActiveAnimTwo=GAPILE_A\nActiveAnimTwoZAdjust=-39\nActiveAnimThree=GAPILE_B\nActiveAnimThreeDamaged=INVISO\t;CAARAY_CD\nActiveAnimThreeZAdjust=-75\n\n[GADEPT]\nRemapable=yes\nNormalized=yes\nCameo=FIXICON\nFoundation=3x3\nAnimActive=0,7,2\nFlat=yes\nBuildup=GADEPTMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nBibShape=GADEPTBB\nActiveAnim=GADEPT_A\nActiveAnimDamaged=GADEPT_AD\nActiveAnimZAdjust=-100\nActiveAnimTwo=GADEPT_B\nActiveAnimTwoZAdjust=-100\nSpecialAnim=GADEPT_C1\nSpecialAnimZAdjust=-100\nSpecialAnimTwo=GADEPT_C2\nSpecialAnimTwoZAdjust=-100\nSpecialAnimThree=GADEPT_C3\nSpecialAnimThreeZAdjust=-100\nProductionAnim=GADEPT_D\nProductionAnimDamaged=GADEPT_DD\nProductionAnimZAdjust=-100\n\n[GAPAVE]\nFoundation=4x4\nCameo=PAVEICON\n;IsNewTheater=yes\n\n[GAGREEN]\n;Cameo=WALLICON\nFoundation=2x2\n\n[GASAND]\nCameo=SBAGICON\nFoundation=1x1\nToOverlay=GASAND\nDamageLevels=2\nNewTheater=yes\n\n[GAWALL]\nCameo=WALLICON\nFoundation=1x1\nToOverlay=GAWALL\nDamageLevels=3\t;2 in Firestorm\nNewTheater=yes\n\n[NAWALL]\nCameo=NWALICON\nFoundation=1x1\nToOverlay=NAWALL\nDamageLevels=3\t;2 in Firestorm\nNewTheater=yes\n\n[CABHUT]\nFoundation=1x1\nNewTheater=yes\n\n[CTDAM]\nFoundation=2x5\nHeight=5\n;NewTheater=no\nActiveAnim=CTDAM_A\nActiveAnimDamaged=CTDAM_AD\nActiveAnimZAdjust=-120\nActiveAnimTwo=CTDAM_B\nActiveAnimTwoDamaged=CTDAM_BD\nActiveAnimTwoZAdjust=-60\nTerrainPalette=yes\nExtraDamageStage=false\n;DemandLoad=true\n\n[UFO]\nFoundation=6x4\nTerrainPalette=yes\nExtraDamageStage=false\nDemandLoad=true\nTheater=yes\nHeight=6\n\n[AMMO01]\nFoundation=1x1\nTerrainPalette=no\nExtraDamageStage=false\nDemandLoad=false\nTheater=no\nHeight=1\n\n[NAPULS]\nCameo=EMPICON\nHeight=2\nRemapable=yes\nNormalized=yes\nFoundation=2x2\nPrimaryFireFLH=0,0,80\nTurretNotExportedOnGround=yes\nPBarrelLength=110\nBuildup=NAPULSMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\n\n[GACTWR]\nImage=GACTWR\nRemapable=yes\nNormalized=yes\nCameo=TOWRICON\nFoundation=1x1\nBuildup=GACTWRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GACTWR_A\nActiveAnimZAdjust=-19\nHeight=2\t;firestorm addition\n\n[GAGATE_A]\nImage=GAGATE_A\nRemapable=yes\nCameo=GATEICON\nFoundation=3x1\nBuildup=GAGATE_A\nSpecialZOverlay=GAGATEZA\nGateStages=9\nNewTheater=yes\n\n[GAGATE_B]\nImage=GAGATE_B\nRemapable=yes\nCameo=GAT2ICON\nFoundation=1x3\nBuildup=GAGATE_B\nSpecialZOverlay=GAGATEZB\nGateStages=9\nNewTheater=yes\n\n[NAGATE_A]\nImage=NAGATE_A\nRemapable=yes\nCameo=NGATICON\nFoundation=3x1\nBuildup=NAGATE_A\nSpecialZOverlay=NAGATEZA\nGateStages=6\nNewTheater=yes\n\n[NAGATE_B]\nImage=NAGATE_B\nRemapable=yes\nCameo=NGA2ICON\nFoundation=1x3\nBuildup=NAGATE_B\nSpecialZOverlay=NAGATEZB\nGateStages=6\nNewTheater=yes\n\n[GALITE]\nCameo=LITEICON\nImage=GALITE\nRemapable=yes\nFoundation=1x1\nTerrainPalette=yes\nNewTheater=yes\nExtraDamageStage=false\n;DemandLoad=yes\n;Theater=yes\n\n[NATMPL]\nImage=NATMPL\nCameo=TMPLICON\nHeight=2\t;was 2.5\nFoundation=4x3\nBuildup=NATMPLMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NATMPL_A\nActiveAnimZAdjust=-127\n\n[NTPYRA]\nImage=NTPYRA\nRemapable=yes\nFoundation=4x4\nNewTheater=yes\nHeight=3\nActiveAnim=NTPYRA_A\nActiveAnimDamaged=NTPYRA_AD\nActiveAnimZAdjust=-30\nDemandLoad=true\n\n[GAKODK]\nImage=GAKODK\nRemapable=yes\nFoundation=4x2\nNewTheater=yes\nHeight=1\nActiveAnim=GAKODK_A\nActiveAnimDamaged=GAKODK_AD\nActiveAnimZAdjust=-60\nActiveAnimTwo=GAKODK_B\nActiveAnimTwoZAdjust=-127\nActiveAnimThree=GAKODK_C\nActiveAnimThreeDamaged=GAKODK_CD\nActiveAnimThreeZAdjust=-127\nDemandLoad=true\n\n[NAMNTK]\nImage=NAMNTK\nRemapable=yes\nFoundation=1x3\nNewTheater=yes\nHeight=2\t;was 1.5\nActiveAnim=NAMNTK_A\nActiveAnimZAdjust=-60\nDemandLoad=true\n\n[GAFSDF_A]\nImage=GAFSDF_A\n;Remapable=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=4\nLoopCount=-1\nRate=250\nSurface=yes\n\n[FSIDLE]\nImage=FSIDLE\nLoopStart=0\nLoopEnd=20\nRate=800\nShouldUseCellDrawer=false\nReport=FIRSTRM1\n\n[FSAIR]\nImage=FSAIR\nLoopStart=0\nLoopEnd=20\nRate=800\nReport=FIRSTRM1\n\n[FSGRND]\nImage=FSGRND\nLoopStart=0\nLoopEnd=20\nRate=800\nReport=FIRSTRM1\nYDrawOffset=-20\n\n[NAWAST]\nCameo=WASTICON\nImage=NAWAST\nRemapable=yes\nFoundation=3x3\nBuildup=NAWASTMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nBibShape=NAWASTBB\nActiveAnim=NAWAST_A\nActiveAnimDamaged=NAWAST_AD\nActiveAnimZAdjust=-127\nActiveAnimTwo=NAWAST_B\nActiveAnimTwoDamaged=NAWAST_BD\nActiveAnimTwoZAdjust=-60\n\n[NAOBEL]\nCameo=OBLIICON\nImage=NAOBEL\nHeight=2\nRemapable=yes\nFoundation=2x2\nChargeAnim=yes\nBuildup=NAOBELMK\nDemandLoadBuildup=true\nFreeBuildup=true\n;PrimaryFirePixelOffset=10,-51\nPrimaryFirePixelOffset=11,-26\nNewTheater=yes\nActiveAnim=NAOBEL_A\nActiveAnimZAdjust=-100\n\n[NAMISL]\nCameo=MSSLICON\nImage=NAMISL\nRemapable=yes\nFoundation=2x2\nBuildup=NAMISLMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAMISL_B\nActiveAnimDamaged=NAMISL_BD\nActiveAnimZAdjust=-100\n\n[GAVULC]\nCameo=TWR1ICON\nImage=GACTWR_B\nRemapable=yes\nFoundation=1x1\nPrimaryFireFLH=162,30,90\nSecondaryFireFLH=162,-30,90\nNewTheater=yes\n\n[GAROCK]\nCameo=TWR2ICON\nImage=GACTWR_C\nRemapable=yes\nFoundation=1x1\nPrimaryFireFLH=152,50,192\nSecondaryFireFLH=152,-50,192\nNewTheater=yes\n\n; SAM addon for component tower\n[GACSAM]\nImage=GACTWR_D\nRemapable=yes\nCameo=TWR3ICON\nFoundation=1x1\nMidPoint=66\nPrimaryFireFLH=152,50,192\nSecondaryFireFLH=152,-50,192\nNewTheater=yes\n\n; SAM site turret\n[NASAM_A]\nImage=GACTWR_D\n;Remapable=yes\n;PrimaryFireFLH=90,50,100\n;SecondaryFireFLH=90,-50,100\n;NewTheater=yes\n\n[GAFIRE]\nCameo=FSDICON\nImage=GAFIRE\nHeight=2\nRemapable=yes\nFoundation=3x2\nBuildup=GAFIREMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=GAFIRE_C\nActiveAnimZAdjust=-100\nActiveAnimTwo=GAFIRE_B\nActiveAnimTwoZAdjust=-75\n\n[GAFSDF]\nImage=GAFSDF\nRemapable=yes\nFoundation=1x1\nNewTheater=yes\nNormalZAdjust=-10\nCameo=FSPICON\n\n[NAPOST]\nCameo=LASRICON\nImage=NAPOST\nRemapable=yes\nFoundation=1x1\nBuildup=NAPOSTMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nActiveAnim=NAPOST_A\n;ActiveAnimDestroyed=NAPOST_AD\nActiveAnimZAdjust=-25\nActiveAnimTwo=NAPOST_B\nActiveAnimTwoZAdjust=-100\n\n[NAFNCE]\nImage=NAFNCE\nRemapable=yes\nFoundation=1x1\nNewTheater=yes\n\n[NALASR]\nCameo=PLTICON\nImage=NALASR\nRemapable=yes\nFoundation=1x1\nBuildup=NALASRMK\nDemandLoadBuildup=true\nFreeBuildup=true\nNewTheater=yes\nPrimaryFireFLH=0,0,168\nRecoilless=yes\nPBarrelLength=200\nPBarrelThickness=48\n\n[NASAM]\nCameo=SAMICON\nImage=NASAM\nRemapable=yes\nFoundation=1x1\nBuildup=NASAMMK\nDemandLoadBuildup=true\nFreeBuildup=true\nPrimaryFireFLH=90,50,100\nSecondaryFireFLH=90,-50,100\nNewTheater=yes\n\n[ABAN01]\nFoundation=2x6\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nHeight=3\nDemandLoad=true\n\n[ABAN02]\nFoundation=5x3\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nHeight=4\t;was 2\nDemandLoad=true\n\n[ABAN03]\nFoundation=2x5\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nHeight=2\nDemandLoad=true\n\n[ABAN04]\nFoundation=4x2\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nDemandLoad=true\nHeight=3\n\n[ABAN05]\nFoundation=3x2\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nDemandLoad=true\nHeight=2\n\n[ABAN06]\nFoundation=2x2\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nDemandLoad=true\nHeight=2\n\n[ABAN07]\nFoundation=2x2\nExtraDamageStage=false\nTerrainPalette=true\n;NewTheater=yes\nDemandLoad=true\nHeight=2\n\n[ABAN08]\nFoundation=2x2\nExtraDamageStage=false\n;NewTheater=yes\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[ABAN09]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN10]\n;NewTheater=yes\nFoundation=2x2\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[ABAN11]\n;NewTheater=yes\nFoundation=2x2\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[ABAN12]\n;NewTheater=yes\nFoundation=2x2\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[ABAN13]\n;NewTheater=yes\nFoundation=1x1\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN14]\n;NewTheater=yes\nFoundation=1x1\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN15]\n;NewTheater=yes\nFoundation=1x1\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN16]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN17]\n;NewTheater=yes\nFoundation=1x1\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[ABAN18]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[CA0001]\nFoundation=3x3\nHeight=1\nNewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CA0002]\nFoundation=3x3\nHeight=1\nNewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CA0003]\nFoundation=2x2\nNewTheater=yes\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CA0004]\nFoundation=2x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0005]\nFoundation=1x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0006]\nFoundation=1x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0007]\nFoundation=1x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0008]\nFoundation=2x3\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0009]\nFoundation=2x3\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0010]\nFoundation=2x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0011]\nFoundation=1x2\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0012]\nFoundation=1x2\nHeight=2\t;was 1.5\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0013]\nFoundation=2x1\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0014]\nFoundation=1x1\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0015]\nFoundation=1x1\nHeight=1\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0016]\nFoundation=1x1\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0017]\nFoundation=1x1\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0018]\nFoundation=1x2\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0019]\nFoundation=1x2\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0020]\nFoundation=1x2\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n[CA0021]\nFoundation=1x2\nHeight=2\nExtraDamageStage=false\nTerrainPalette=true\nNewTheater=yes\nDemandLoad=true\n\n; Ruined C&C con yard\n[GAOLDCC1]\nFoundation=2x2\nHeight=1\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n; Ruined C&C Temple of NOD\n[GAOLDCC2]\nFoundation=2x2\nHeight=2\t;was 1.5\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n; Ruined C&C Weapons factory.\n[GAOLDCC3]\nFoundation=2x2\nHeight=2\t;was 1.5\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n; Ruined C&C Refinery.\n[GAOLDCC4]\nFoundation=2x2\nHeight=2\t;was 1.5\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n; Ruined C&C Advanced Power.\n[GAOLDCC5]\nFoundation=2x2\nHeight=1\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n; Ruined C&C Silos (2)\n[GAOLDCC6]\nFoundation=2x2\nHeight=1\nExtraDamageStage=false\nNewTheater=yes\nRemapable=yes\nDemandLoad=true\n\n[CITY01]\nFoundation=4x2\nHeight=3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY02]\nFoundation=2x3\nHeight=3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY03]\nFoundation=3x2\nHeight=3\t;was 2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY04]\nFoundation=3x2\nHeight=4\t;was 3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY05]\nFoundation=3x2\nHeight=5\t;was 4\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY06]\nFoundation=4x2\nHeight=3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY07]\nFoundation=4x2\nHeight=3\t;was 2.5\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY08]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY09]\nFoundation=2x2\nHeight=2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY10]\nFoundation=2x2\nHeight=2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY11]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nHeight=4\nDemandLoad=true\n\n[CITY12]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nHeight=4\nDemandLoad=true\n\n[CITY13]\nFoundation=2x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nHeight=4\nDemandLoad=true\n\n[CITY14]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nHeight=4\nDemandLoad=true\n\n[CITY15]\nFoundation=4x2\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\nHeight=2\n\n[CITY16]\nFoundation=4x2\nHeight=3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY17]\nFoundation=4x3\nHeight=5\t;was 4.5\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY18]\nFoundation=3x5\nHeight=4\t;was 3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY19]\nFoundation=2x2\nHeight=1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY20]\nFoundation=1x1\nHeight=1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY21]\nFoundation=1x1\nHeight=2\t;was 1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CITY22]\nFoundation=2x2\nHeight=2\t;was 3\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[CTVEGA]\nFoundation=4x4\nHeight=3\nNewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD01]\nFoundation=1x1\n;NewTheater=yes\nTerrainPalette=true\nExtraDamageStage=false\nDemandLoad=true\n\n[BBOARD02]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD03]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD04]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD05]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD06]\nFoundation=1x2\n;NewTheater=yes\nTerrainPalette=true\nExtraDamageStage=false\nDemandLoad=true\n\n[BBOARD07]\nFoundation=1x2\n;NewTheater=yes\nTerrainPalette=true\nExtraDamageStage=false\nDemandLoad=true\n\n[BBOARD08]\nFoundation=1x2\n;NewTheater=yes\nTerrainPalette=true\nExtraDamageStage=false\nDemandLoad=true\n\n[BBOARD09]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD10]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD11]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD12]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD13]\nFoundation=1x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD14]\nFoundation=2x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD15]\nFoundation=2x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BBOARD16]\nFoundation=2x1\n;NewTheater=yes\nExtraDamageStage=false\nTerrainPalette=true\nDemandLoad=true\n\n[BOXES01]\nFoundation=1x1\nTheater=yes\n\n[BOXES02]\nFoundation=1x1\nTheater=yes\n\n[BOXES03]\nFoundation=1x1\nTheater=yes\n\n[BOXES04]\nFoundation=1x1\nTheater=yes\n\n[BOXES05]\nFoundation=1x1\nTheater=yes\n\n[BOXES06]\nFoundation=1x1\nTheater=yes\n\n[BOXES07]\nFoundation=1x1\nTheater=yes\n\n[BOXES08]\nFoundation=1x1\nTheater=yes\n\n[BOXES09]\nFoundation=1x1\nTheater=yes\n\n[ICE01]\nFoundation=2x2\nTheater=yes\n\n[ICE02]\nFoundation=1x2\nTheater=yes\n\n[ICE03]\nFoundation=2x1\nTheater=yes\n\n[ICE04]\nFoundation=1x1\nTheater=yes\n\n[ICE05]\nFoundation=1x1\nTheater=yes\n\n[TIBTRE01]\nTheater=yes\nFoundation=1x1\n\n[TIBTRE02]\nTheater=yes\nFoundation=1x1\n\n[TIBTRE03]\nTheater=yes\nFoundation=1x1\n\n[TREE01]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE02]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE03]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE04]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE05]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE06]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE07]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE08]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE09]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE10]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE11]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE12]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE13]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE14]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE15]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE16]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE17]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE18]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE19]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE20]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE21]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE22]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE23]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE24]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n[TREE25]\nTheater=yes\nFoundation=1x1\n;DemandLoad=true\n\n; Smudges listed here\n[CRATER01]\nTheater=yes\n\n[CRATER02]\nTheater=yes\n\n[CRATER03]\nTheater=yes\n\n[CRATER04]\nTheater=yes\n\n[CRATER05]\nTheater=yes\n\n[CRATER06]\nTheater=yes\n\n[CRATER07]\nTheater=yes\n\n[CRATER08]\nTheater=yes\n\n[CRATER09]\nTheater=yes\n\n[CRATER10]\nTheater=yes\n\n[CRATER11]\nTheater=yes\n\n[CRATER12]\nTheater=yes\n\n[BURNT01]\nTheater=yes\n\n[BURNT02]\nTheater=yes\n\n[BURNT03]\nTheater=yes\n\n[BURNT04]\nTheater=yes\n\n[BURNT05]\nTheater=yes\n\n[BURNT06]\nTheater=yes\n\n[BURNT07]\nTheater=yes\n\n[BURNT08]\nTheater=yes\n\n[BURNT09]\nTheater=yes\n\n[BURNT10]\nTheater=yes\n\n[BURNT11]\nTheater=yes\n\n[BURNT12]\nTheater=yes\n\n;[CR1]\n;Theater=yes\n\n;[CR2]\n;Theater=yes\n\n;[CR3]\n;Theater=yes\n\n;[CR4]\n;Theater=yes\n\n;[CR5]\n;Theater=yes\n\n;[CR6]\n;Theater=yes\n\n;[BURN01]\n;Theater=yes\n\n;[BURN02]\n;Theater=yes\n\n;[BURN03]\n;Theater=yes\n\n;[BURN04]\n;Theater=yes\n\n;[BURN05]\n;Theater=yes\n\n;[BURN06]\n;Theater=yes\n\n;[BURN07]\n;Theater=yes\n\n;[BURN08]\n;Theater=yes\n\n;[BURN09]\n;Theater=yes\n\n;[BURN10]\n;Theater=yes\n\n;[BURN11]\n;Theater=yes\n\n;[BURN12]\n;Theater=yes\n\n;[BURN13]\n;Theater=yes\n\n;[BURN14]\n;Theater=yes\n\n;[BURN15]\n;Theater=yes\n\n;[BURN16]\n;Theater=yes\n\n[BRIDGE]\nTheater=yes\n\n[CRATE]\nTheater=yes\n\n[RAILBRDG]\nTheater=yes\n\n[TIB01]\nTheater=yes\n\n[TIB02]\nTheater=yes\n\n[TIB03]\nTheater=yes\n\n[TIB04]\nTheater=yes\n\n[TIB05]\nTheater=yes\n\n[TIB06]\nTheater=yes\n\n[TIB07]\nTheater=yes\n\n[TIB08]\nTheater=yes\n\n[TIB09]\nTheater=yes\n\n[TIB10]\nTheater=yes\n\n[TIB11]\nTheater=yes\n\n[TIB12]\nTheater=yes\n\n[TIB13]\nTheater=yes\n\n[TIB14]\nTheater=yes\n\n[TIB15]\nTheater=yes\n\n[TIB16]\nTheater=yes\n\n[TIB17]\nTheater=yes\n\n[TIB18]\nTheater=yes\n\n[TIB19]\nTheater=yes\n\n[TIB20]\nTheater=yes\n\n;[BTIB01]\n;Theater=yes\n\n;[BTIB02]\n;Theater=yes\n\n;[BTIB03]\n;Theater=yes\n\n[TRACKS01]\nTheater=yes\nDemandLoad=true\n\n[TRACKS02]\nTheater=yes\nDemandLoad=true\n\n[TRACKS03]\nTheater=yes\nDemandLoad=true\n\n[TRACKS04]\nTheater=yes\nDemandLoad=true\n\n[TRACKS05]\nTheater=yes\nDemandLoad=true\n\n[TRACKS06]\nTheater=yes\nDemandLoad=true\n\n[TRACKS07]\nTheater=yes\nDemandLoad=true\n\n[TRACKS08]\nTheater=yes\nDemandLoad=true\n\n[TRACKS09]\nTheater=yes\nDemandLoad=true\n\n[TRACKS10]\nTheater=yes\nDemandLoad=true\n\n[TRACKS11]\nTheater=yes\nDemandLoad=true\n\n[TRACKS12]\nTheater=yes\nDemandLoad=true\n\n[TRACKS13]\nTheater=yes\nDemandLoad=true\n\n[TRACKS14]\nTheater=yes\nDemandLoad=true\n\n[TRACKS15]\nTheater=yes\nDemandLoad=true\n\n[TRACKS16]\nTheater=yes\nDemandLoad=true\n\n[SROCK01]\nTheater=yes\n\n[SROCK02]\nTheater=yes\n\n[SROCK03]\nTheater=yes\n\n[SROCK04]\nTheater=yes\n\n[SROCK05]\nTheater=yes\n\n[TROCK01]\nTheater=yes\n\n[TROCK02]\nTheater=yes\n\n[TROCK03]\nTheater=yes\n\n[TROCK04]\nTheater=yes\n\n[TROCK05]\nTheater=yes\n\n[TRTUNN01]\n;TerrainPalette=yes\nTheater=yes\n;Foundation=0x0\nDemandLoad=true\n\n[TRTUNN02]\n;TerrainPalette=yes\nTheater=yes\n;Foundation=0x0\nDemandLoad=true\n\n[TRTUNN03]\n;TerrainPalette=yes\nTheater=yes\n;Foundation=0x0\nDemandLoad=true\n\n[TRTUNN04]\n;TerrainPalette=yes\nTheater=yes\n;Foundation=0x0\nDemandLoad=true\n\n[VEINS]\nTheater=yes\nDemandLoad=true\n\n[VEINHOLE]\nTheater=yes\n\n[VEINATAC]\nTheater=yes\nRate=300\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nIsVeins=true\nDemandLoad=true\n\n[LOBRDG01]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG02]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG03]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG04]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG05]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG06]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG07]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG08]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG09]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG10]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG11]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG12]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG13]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG14]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG15]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG16]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG17]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG18]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG19]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG20]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG21]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG22]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG23]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG24]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG25]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG26]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG27]\nTheater=yes\nDemandLoad=true\n\n[LOBRDG28]\nTheater=yes\nDemandLoad=true\n\n[LOBRDGE1]\nTheater=yes\nDemandLoad=true\n\n[LOBRDGE2]\nTheater=yes\nDemandLoad=true\n\n[LOBRDGE3]\nTheater=yes\nDemandLoad=true\n\n[LOBRDGE4]\nTheater=yes\nDemandLoad=true\n\n[FPLS]\n\n;[WCRATE]\n\n;[WWCRATE]\n\n;[SCRATE]\n\n\n; *** Infantry Sequences ***\n; Infantry animations are grouped within a single art file.\n; Unlike units, infantry animation layout is completely\n; arbitrary and must be explicitly specified. Each\n; infantry format file will be identified with one of these\n; animation sequences.\n;\n; The first number is the starting frame number. The second\n; number is the number of frames of the animation. If this\n; number is zero then the anim sequence is not present.\n; The third number is the multiplier by the infantry facing\n; to reach the facing specific animation start. If this\n; last number is zero, then there is no facing specific\n; version.\n;\n; Ready = standing around\n; Guard = standing around with weapon drawn\n; Prone = while prone\n; Walk = walking [normal movement]\n; FireUp = firing while standing\n; Down = transition from standing to prone\n; Crawl = moving while prone\n; Up = transition from prone to standing\n; FireProne = firing while prone\n; Idle1 = idle animation sequence #1\n; Idle2 = idle animation sequence #2\n; Die1 = death animation when hit by gunfire\n; Die2 = death animation when exploding\n; Die3 = death animation when exploding (alternate)\n; Die4 = death animation by concussion explosion\n; Die5 = death animatino by fire\n; Fly = [jumpjet] flying\n; Hover = [jumpjet] hovering\n; Tumble = [jumpjet] tumbling\n; FireFly = [jumpjet] firing while flying\n\n\n[DoggieSequence]\nReady=0,1,1\nGuard=0,1,1\nProne=90,1,0\nWalk=8,6,6\nFireUp=56,4,4\nDown=88,3,0\nCrawl=0,0,0\nUp=89,1,0\nFireProne=0,0,0\nIdle1=91,8,0,E\nIdle2=91,8,0,E\nDie1=99,10,0\nDie2=99,10,0\nDie3=99,10,0\nDie4=99,10,0\nDie5=109,10,0\nStruggle=0,6,0\t;firestorm addition\n\n[E1Sequence]\nReady=0,1,1\nGuard=0,1,1\nProne=86,1,6\nWalk=8,6,6\nFireUp=164,6,6\nDown=260,2,2\nCrawl=86,6,6\nUp=276,2,2\nFireProne=212,6,6\nIdle1=56,15,0,W\nIdle2=71,14,0,E\nDie1=134,15,0\nDie2=149,15,0\nDie3=0,1,1\nDie4=0,1,1\nDie5=0,1,1\nStruggle=0,6,0\t;firestorm addition\n\n[MedicSequence]\nReady=0,1,1\nGuard=0,1,1\nProne=86,1,6\nWalk=8,6,6\nFireUp=292,15,0\nDown=260,2,2\nCrawl=86,6,6\nUp=276,2,2\nFireProne=292,15,0\nIdle1=56,15,0,W\nIdle2=71,14,0,E\nDie1=134,15,0\nDie2=149,15,0\nDie3=0,1,1\nDie4=0,1,1\nDie5=0,1,1\nStruggle=0,6,0\t;firestorm addition\n\n[JumpjetSequence]\nReady=0,1,1\nGuard=0,1,1\nProne=0,1,1\nWalk=8,6,6\nFireUp=164,6,6\nDown=0,1,1\nCrawl=8,6,6\nUp=0,1,1\nFireProne=164,6,6\nIdle1=56,15,0,W\nIdle2=71,15,0,E\nDie1=134,15,0\nDie2=149,15,0\nDie3=0,0,0\nDie4=0,0,0\nDie5=0,0,0\nFly=292,6,6\nHover=340,6,6\nFireFly=388,6,6\nTumble=436,15,0\nStruggle=0,6,0\t;firestorm addition\n\n[CyborgSequence]\nReady=0,1,1\nGuard=0,1,1\nProne=110,1,9\nWalk=8,9,9\nFireUp=212,6,6\nDown=0,1,1\nCrawl=110,9,9\nUp=0,1,1\nFireProne=260,6,6\nIdle1=80,15,0,W\nIdle2=95,15,0,E\nDie1=182,15,0\nDie2=197,15,0\nDie3=0,0,0\nDie4=0,0,0\nDie5=0,0,0\nStruggle=0,6,0\t;firestorm addition\n\n\n; *** Projectile Objects ***\n; Projectiles sometimes need additional information regarding their\n; imagery.\n\n; Trailer = animation to spawn as the projectile moves [typically smoke] (def=none)\n; Rotates = Does projectile have specific imagery according to facing (def=no)?\n; Frames = number of image frames for animation purposes (def=1)\n; AnimPalette = Does it use the animation palette palette (def=no)?\n[120MM]\n\n[DRAGON]\nTrailer=SMOKEY2\nRotates=yes\n\n[MISLCHEM]\nTrailer=SMOKEY2\nVoxel=yes\n\n[MISLMLTI]\nTrailer=SMOKEY2\nVoxel=yes\n\n[TORPEDO]\nAnimPalette=yes\nAnimLow=0\nAnimHigh=2\nAnimRate=1\n\n[DISCUS]\nAnimPalette=yes\nAnimLow=0\nAnimHigh=5\nAnimRate=1\n\n[CANISTER]\nAnimLow=0\nAnimHigh=13\nAnimRate=1\n\n[BOMB]\nAnimPalette=yes\nAnimLow=0\nAnimHigh=13\nAnimRate=1\n\n[MISSILE]\nTrailer=SMOKEY2\nRotates=yes\n\n[PATRIOT]\nTrailer=SMOKEY\nRotates=yes\n\n\n; *** Animation Overlays ***\n; These are the temporary animations overlays that are used for such\n; effects as explosions, smoke, and fire.\n\n; Theater = Is there theater specific art for this animation (def=no)?\n; Normalized = Should the animation speed be adjusted to appear consistent (def=no)?\n; Scorch = Does this animation scorch the ground [e.g., napalm does this] (def=no)?\n; Flamer = Does this animation leave flames after it is gone [e.g., napalm] (def=no)?\n; Crater = Does this form a crater [e.g., artillery does this] (def=no)?\n; Sticky = Animation sticks to unit in square (def=no)?\n; Surface = Is this animation at ground level (def=no)?\n; Flat = Is animation flat on the ground [helps with drawing logic to know this] (def=no)?\n; Translucent = Is this animation translucent in appearence (def=no)?\n; Translucency = percent of translucency to use [25, 50, 75% only] (def=none)\n; Damage = damage to apply per minute to attached object [if any] (def=0)\n; Rate = desired animation frames per minute (def=900)\n; Report = sound effect to play when this animation plays (def=none)\n; Next = animation to spawn when this animation completes [fire uses this to get smaller over time] (def=none>\n; DetailLevel = The detail level that the game must be set to for this animation to play (def=0, 2 is high detail).\n; Start = Frame to start this animation from.\n; UseNormalLight = Does this anim always draw at 100% brightness? (def=no)\n; YSortAdjust = Fudge to apply when sorting this object in it's layer (def=0).\n; AltPalette = Does it use an alternate drawing palette (def=no)?\n;  <<< these values are needed if the animation loops >>>\n;    LoopCount = number of times this animation loops before ending (def=0)?\n;    LoopStart = beginning frame of loop [if animation loops]\n;    LoopEnd = last frame of loop [if animation loops] (def=last frame of animation)\n;  <<< values used only for wave-based animations\n;    Angle = angle in degrees to use for the cone of effect (between 0 and 180 please)\n\n[BEACON]\nSurface=yes\nLoopCount=100\nRate=60\n\n[WAKE1]\n;Theater=yes\nFlat=yes\nSurface=yes\nTranslucent=yes\nRate=120\nYSortAdjust=-64\n\n[WAKE2]\n;Theater=yes\nFlat=yes\nSurface=yes\nTranslucent=yes\nRate=120\nYSortAdjust=-64\n\n[STEAMPUF]\nFlat=yes\nSurface=yes\nTranslucent=yes\n\n[DROPPOD]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\nTranslucent=yes\n\n[DROPPOD2]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\nTranslucent=yes\n\n[DEATH_A]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DEATH_B]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DEATH_C]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DEATH_D]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DEATH_E]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DEATH_F]\nRate=10\nFlat=yes\nDetailLevel=1\nSurface=yes\n\n[DBRIS1LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\nTrailerAnim=SMOKEY2\nTrailerSeperation=2\n\n[DBRIS1SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS2LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS2SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS3LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS3SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS4LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS4SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS5LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\nTrailerAnim=SMOKEY2\nTrailerSeperation=2\n\n[DBRIS5SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS6LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS6SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS7LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS7SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS8LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\nTrailerAnim=SMOKEY2\nTrailerSeperation=2\n\n[DBRIS8SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS9LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRIS9SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRS10LG]\nElasticity=0.0\nMaxXYVel=25.0\nMinZVel=25.0\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=80\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[DBRS10SM]\nElasticity=0.0\nMaxXYVel=30.0\nMinZVel=20.0\nExpireAnim=TWLT026\nDamage=10\nDamageRadius=50\nWarhead=HE\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRandomRate=220,600\nDetailLevel=0\nBouncer=yes\n\n[TREESPRD]\nNormalized=yes\n\n; Tesla Coil zap animation (infantry)\n[ELECTRO]\nScorch=yes\nSurface=yes\nLoopEnd=3\nLoopCount=5\nNext=FIRE1\nReport=ELECTRO1\n\n; SAM site fire animation\n[SAM-N]\n\n[SAM-NE]\n\n[SAM-NW]\n\n[SAM-E]\n\n[SAM-W]\n\n[SAM-SW]\n\n[SAM-SE]\n\n[SAM-S]\n\n[DIG]\nSurface=yes\n\n[INFDIE]\nNormalized=yes\nSurface=yes\n\n[DIRTEXPL]\nNormalized=yes\nSurface=yes\nTranslucent=yes\nReport=EXPDIRT1\n\n;[PULSGRNL]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n;[PULSGRNS]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n;[PULSREDL]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n;[PULSREDS]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n;[PULSWHTL]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n;[PULSWHTS]\n;Normalized=yes\n;Surface=yes\n;Translucent=yes\n\n[PULSEFX1]\nNormalized=yes\nSurface=yes\nTranslucent=yes\nUseNormalLight=yes\n;AltPalette=yes\n;Translucency=50\n\n[PULSEFX2]\nNormalized=yes\nSurface=yes\nTranslucent=yes\nUseNormalLight=yes\n;AltPalette=yes\n;Translucency=50\n\n[EMP_FX01]\nNormalized=yes\nSurface=yes\nUseNormalLight=yes\nLoopCount=-1\n\n; smoke used as landing zone marker\n[SMOKLAND]\nNormalized=yes\nSurface=yes\nLoopStart=72\nLoopEnd=91\nLoopCount=255\n\n[SGRYSMK1]\nRate=500\nNormalized=yes\nTranslucent=yes\n\n; small sticky fire\n[BURN-S]\nSurface=yes\nSticky=yes\nDamage=.03\nRate=400\nLoopStart=30\nLoopEnd=62\nLoopCount=4\nUseNormalLight=yes\n;Next=SMOKE_M\nTranslucency=25\n\n; medium sticky fire\n[BURN-M]\nSurface=yes\nSticky=yes\nDamage=.06\nRate=400\nLoopStart=30\nLoopEnd=62\nLoopCount=4\nNext=BURN-S\nUseNormalLight=yes\nTranslucency=25\n\n; large sticky fire\n[BURN-L]\nSurface=yes\nSticky=yes\nDamage=.06\nRate=400\nLoopStart=30\nLoopEnd=62\nLoopCount=4\nNext=BURN-M\nUseNormalLight=yes\nTranslucency=25\n\n; parachute to attach to parachutists\n[PARACH]\nRate=200\nLoopStart=7\nLoopCount=15\n\n; parachute bomb\n[PARABOMB]\nRate=200\nLoopStart=7\nLoopCount=15\nAnimLow=8\nAnimHigh=12\nAnimRate=1\n\n[TWLT026]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW05\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT036]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW06\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT050]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW07\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT070]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW09\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT070T]\nImage=TWLT070\nNormalized=yes\nTranslucent=yes\nTiberiumChainReaction=yes\nReport=EXPNEW05\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT100]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW10\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[TWLT100I]\nImage=TWLT100\nNormalized=yes\nTranslucency=50\nReport=EXPNEW11\nUseNormalLight=yes\n\n[RING1]\nTranslucent=yes\nRate=300\nTranslucencyDetailLevel=1\nFlat=true\n\n[IONBEAM]\nTranslucent=yes\nRate=200\nTiled=yes\nTranslucencyDetailLevel=1\nReport=ION1\n\n[S_BANG16]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW10\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BANG24]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW10\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BANG34]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW10\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BANG48]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW11\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BRNL20]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW12\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BRNL30]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW12\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BRNL40]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW12\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_BRNL58]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW12\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_CLSN16]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW14\nUseNormalLight=yes\nCrater=yes\n\n[S_CLSN22]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW14\nUseNormalLight=yes\nCrater=yes\n\n[S_CLSN30]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW14\nUseNormalLight=yes\nCrater=yes\n\n[S_CLSN42]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW14\nUseNormalLight=yes\nCrater=yes\n\n[S_CLSN58]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW14\nUseNormalLight=yes\nCrater=yes\n\n[S_TUMU22]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW15\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_TUMU30]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW15\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_TUMU42]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW15\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n[S_TUMU60]\nNormalized=yes\nTranslucent=yes\nReport=EXPNEW15\nUseNormalLight=yes\nCrater=yes\nScorch=yes\n\n; smoke puff used by rockets\n[SMOKEY]\nTranslucent=yes\n\n[SMOKEY2]\nTranslucent=yes\n\n; small arms fire piff (single shot)\n[PIFF]\n;Normalized=yes\n;Theater=yes\n;Translucent=yes\n\n; small arms fire piff (multiple shots)\n[PIFFPIFF]\n;Theater=yes\n;Normalized=yes\n;Translucent=yes\n\n; small flames\n[FIRE3]\nSurface=yes\nDamage=.003\nLoopCount=5\nRate=450\nUseNormalLight=yes\nTranslucency=50\n\n; medium flames\n[FIRE1]\nScorch=yes\nSurface=yes\nDamage=.006\nRate=450\nLoopCount=4\nUseNormalLight=yes\nTranslucency=50\nNext=FIRE2\n\n; medium flames\n[FIRE2]\nScorch=yes\nSurface=yes\nDamage=.006\nRate=450\nLoopCount=6\nUseNormalLight=yes\nTranslucency=50\nNext=FIRE3\n\n; tiny flames\n[FIRE4]\nSurface=yes\nDamage=.002\nLoopCount=3\nUseNormalLight=yes\nTranslucency=25\n\n; muzzle flash\n[GUNFIRE]\nSurface=yes\nTranslucent=yes\n\n[XGRYSML1]\nTranslucent=yes\nReport=EXPNEW13\n\n[XGRYSML2]\nTranslucent=yes\nReport=EXPNEW13\n\n[XGRYMED1]\nTranslucent=yes\nReport=EXPNEW15\n\n[XGRYMED2]\nTranslucent=yes\nReport=EXPNEW12\n\n[EXPLOLRG]\nTranslucent=yes\nUseNormalLight=yes\nReport=EXPNEW09\nCrater=yes\nScorch=yes\n\n[PODRING]\nTranslucent=yes\nFlat=yes\n\n[CLDRNGL1]\nSurface=yes\nFlat=yes\n\n[CLDRNGL2]\nSurface=yes\nFlat=yes\n\n[CLDRNGMD]\nSurface=yes\nFlat=yes\n\n[CLDRNGSM]\nSurface=yes\nFlat=yes\n\n[DROPEXP]\nSurface=yes\nTranslucent=yes\n\n[INVISO]\nSurface=yes\nDamage=1\n\n[EXPLOMED]\nTranslucent=yes\nUseNormalLight=yes\nReport=EXPNEW12\nCrater=yes\nScorch=yes\n\n[EXPLOSML]\nTranslucent=yes\nUseNormalLight=yes\nReport=EXPNEW13\nCrater=yes\nScorch=yes\n\n; minigun fire flashes\n[MGUN-N]\n\n[MGUN-NE]\n\n[MGUN-E]\n\n[MGUN-SE]\n\n[MGUN-S]\n\n[MGUN-SW]\n\n[MGUN-W]\n\n[MGUN-NW]\n\n; Armor bonus\n[ARMOR]\nNormalized=yes\nRate=400\n\n; Money bonus\n[MONEY]\nNormalized=yes\nRate=400\n\n; Firepower bonus crate animation\n[FIREPOWR]\nNormalized=yes\nRate=400\n\n[VETERAN]\nNormalized=yes\nRate=400\n\n[REVEAL]\nNormalized=yes\nRate=400\n\n[SHROUDX]\nNormalized=yes\nRate=400\n\n[MLTIMISL]\nNormalized=yes\nRate=400\n\n[HEALONE]\nNormalized=yes\nRate=400\n\n[HEALALL]\nNormalized=yes\nRate=400\n\n[CHEMISLE]\nNormalized=yes\nRate=400\n\n[CLOAK]\nNormalized=yes\nRate=400\n\n; twinkle animation\n[TWINKLE1]\nNormalized=yes\nLoopCount=5\n\n[TWINKLE2]\nNormalized=yes\nLoopCount=7\n\n[TWINKLE3]\nNormalized=yes\nLoopCount=3\n\n; large water explosion\n[H2O_EXP1]\nNormalized=yes\nSurface=yes\nAltPalette=yes\nTranslucency=50\nReport=SSPLASH1\nYDrawOffset=-18\n\n; medium water explosion\n[H2O_EXP2]\nNormalized=yes\nSurface=yes\nAltPalette=yes\nTranslucency=50\nReport=SSPLASH2\nYDrawOffset=-10\n\n; small water explosion\n[H2O_EXP3]\nNormalized=yes\nSurface=yes\nAltPalette=yes\nTranslucency=50\nReport=SSPLASH3\nYDrawOffset=-8\n\n; expanding fire ring\n[RING]\nNormalized=yes\nTranslucent=yes\n\n; Power plant active animation\n[GAPOWR_A]\nImage=GAPOWR_A\nNormalized=yes\nNewTheater=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=220\nDetailLevel=1\n\n; Power plant active animation\n[GAPOWR_AD]\nImage=GAPOWR_A\nNormalized=yes\n;NewTheater=yes\nSurface=yes\nStart=12\nLoopStart=12\nLoopEnd=24\nLoopCount=-1\nRate=220\nDetailLevel=1\n\n; Power plant power-up animations\n[GAPOWR_B]\nCameo=TURBICON\nImage=GAPOWR_B\nNewTheater=yes\nNormalized=yes\nSurface=yes\nLoopEnd=12\nLoopCount=-1\nRate=220\n\n; NOD Power plant active animation\n[NAPOWR_A]\nImage=NAPOWR_A\nNormalized=yes\nNewTheater=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=9\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n; NOD Power plant damaged active animation\n[NAPOWR_AD]\nImage=NAPOWR_A\nNormalized=yes\n;NewTheater=yes\nSurface=yes\nStart=9\nLoopStart=9\nLoopEnd=18\nLoopCount=-1\nRate=220\nDetailLevel=1\n\n; NOD Advanced power plant active animation\n[NAAPWR_A]\nImage=NAAPWR_A\nNormalized=yes\nNewTheater=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=9\nLoopCount=-1\nRate=220\nDetailLevel=1\n\n; NOD Advanced power plant damaged active animation\n[NAAPWR_AD]\nImage=NAAPWR_A\nNormalized=yes\n;NewTheater=yes\nSurface=yes\nStart=9\nLoopStart=9\nLoopEnd=18\nLoopCount=-1\nRate=220\nDetailLevel=1\n\n; Civilian Hospital\n[CAHOSP_A]\nImage=CAHOSP_A\nNormalized=yes\nNewTheater=yes\nSurface=yes\nLoopStart=0\nLoopEnd=9\nLoopCount=-1\nRate=350\nDetailLevel=2\nDemandLoad=true\n\n; Civilian Armory\n[CAARMR_A]\nImage=CAARMR_A\nNormalized=yes\nNewTheater=yes\nSurface=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nDemandLoad=true\nDetailLevel=2\n\n[NARADR_A]\nImage=NARADR_A\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=220\nSurface=yes\n\n[NARADR_AD]\nImage=NARADR_A\n;NewTheater=yes\nNormalized=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=180\nSurface=yes\n\n[GARADR_A]\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=28\nLoopCount=-1\nRate=220\n;Surface=yes\nPingPong=no\n\n[GARADR_AD]\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=28\nLoopCount=-1\nRate=180\n;Surface=yes\nPingPong=no\n\n[GAPLUG_A]\nImage=GAPLUG_A\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nRate=220\nSurface=yes\n\n[GAPLUG_B]\nImage=GAPLUG_B\nNewTheater=yes\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=220\nSurface=yes\nPingPong=no\n\n[GAPLUG_BD]\nImage=GAPLUG_B\n;NewTheater=yes\nNormalized=yes\nStart=10\nLoopStart=10\nLoopEnd=20\nLoopCount=-1\nRate=180\nSurface=yes\nPingPong=no\n\n[GAPLUG_C]\nImage=GAPLUG_C\nNewTheater=yes\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nSurface=yes\nPingPong=no\n\n[GAPLUG_D]\nImage=GAPLUG_D\nNewTheater=yes\nCameo=RAD1ICON\nNormalized=yes\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=220\n;Surface=yes\n\n[GAPLUG_E]\nImage=GAPLUG_E\nNewTheater=yes\nCameo=RAD2ICON\nNormalized=yes\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=220\n;Surface=yes\n\n[GAPLUG_F]\nImage=GAPLUG_F\nNewTheater=yes\nCameo=RAD3ICON\nNormalized=yes\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=220\n;Surface=yes\nPingPong=yes\n\n[GAPILE_A]\nImage=GAPILE_A\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=300\nSurface=yes\nDetailLevel=1\n\n[GAPILE_B]\nImage=GAPILE_B\nNewTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=300\nSurface=yes\nDetailLevel=1\n\n[GAPILE_C]\nImage=GAPILE_C\nNormalized=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=7\nLoopCount=-1\nRate=220\nSurface=yes\n\n[GAPILE_CD]\nImage=GAPILE_C\nNormalized=yes\n;NewTheater=yes\nStart=7\nLoopStart=7\nLoopEnd=14\nLoopCount=-1\nRate=180\nSurface=yes\n\n[GAWEAP_A]\nNormalized=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n[GAWEAP_B]\nNormalized=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n[GAWEAP_C]\nNormalized=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=4\nLoopCount=-1\nRate=800\nSurface=yes\nDetailLevel=1\n\n[NAWEAP_A]\nImage=NAWEAP_A\nNormalized=yes\nNewTheater=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n[NAWEAP_AD]\nImage=NAWEAP_A\nNormalized=yes\nNewTheater=yes\nStart=16\nLoopStart=16\nLoopEnd=32\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n[NAPULS_A]\nLoopStart=0\nNewTheater=yes\nLoopEnd=64\nLoopCount=-1\nRate=0\nSurface=yes\n\n[GACTWR_A]\nLoopStart=0\nLoopEnd=6\nNewTheater=yes\nLoopCount=-1\nRate=220\nSurface=yes\nNormalized=yes\nDetailLevel=1\n\n; Component tower double gun turret\n[GACTWR_B]\nLoopStart=0\nLoopEnd=32\nLoopCount=-1\nRate=0\nSurface=yes\nNewTheater=yes\n\n; Component tower rocket launcher\n[GACTWR_C]\nLoopStart=0\nLoopEnd=32\nLoopCount=-1\nRate=0\nSurface=yes\nNewTheater=yes\n\n; Component tower SAM\n[GACTWR_D]\nLoopStart=0\nLoopEnd=32\nLoopCount=-1\nRate=0\nSurface=yes\nNewTheater=yes\n\n; Active animation for stealth generator\n[NASTLH_A]\nNormalized=yes\nLoopStart=0\nLoopEnd=6\nLoopCount=-1\nRate=350\nSurface=yes\nPingPong=no\nNewTheater=yes\nDetailLevel=1\n\n; Damaged active animation for stealth generator\n[NASTLH_AD]\nNormalized=yes\nLoopStart=0\nLoopEnd=6\nLoopCount=-1\nRate=220\nSurface=yes\nPingPong=no\nNewTheater=yes\nDetailLevel=1\n\n; Active animation for construction yard\n[GACNST_A]\nImage=GACNST_A\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Damaged active animation for construction yard\n[GACNST_AD]\nImage=GACNST_A\nNormalized=yes\nStart=10\nLoopStart=10\nLoopEnd=20\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Third active animation for construction yard\n[GACNST_B]\nImage=GACNST_B\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Second active animation for construction yard.\n[GACNST_C]\nImage=GACNST_C\nNormalized=yes\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Damaged second active animation for construction yard.\n[GACNST_CD]\nImage=GACNST_C\nNormalized=yes\nStart=15\nLoopStart=15\nLoopEnd=30\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Production anim for construction yard\n[GACNST_D]\nImage=GACNST_D\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=1\nRate=350\nSurface=yes\nNewTheater=yes\n;Report=FACBLD1\n\n; Active animation for hand of nod\n[NAHAND_A]\nImage=NAHAND_A\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\nActiveAnimTwoZAdjust=-100\n\n; Second active animation for hand of nod\n[NAHAND_B]\nImage=NAHAND_B\nNormalized=yes\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Second active animation for hand of nod - damaged version\n[NAHAND_BD]\nImage=NAHAND_B\nNormalized=yes\nStart=12\nLoopStart=12\nLoopEnd=24\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Active animation for Temple of NOD\n[NATMPL_A]\nImage=NATMPL_A\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n; Active animation for NOD Pyramid\n[NTPYRA_A]\nImage=NTPYRA_A\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n; Damaged active animation for NOD Pyramid\n[NTPYRA_AD]\nImage=NTPYRA_A\nNormalized=yes\nStart=16\nLoopStart=16\nLoopEnd=32\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\n\n; Active animation for NOD Montauk\n[NAMNTK_A]\nImage=NAMNTK_A\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDemandLoad=true\n\n; Active animation for GDI Kodiak\n[GAKODK_A]\nImage=GAKODK_A\nNormalized=yes\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDemandLoad=true\n\n; Damaged active animation for GDI Kodiak\n[GAKODK_AD]\nImage=GAKODK_A\nNormalized=yes\nStart=12\nLoopStart=12\nLoopEnd=24\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\nDemandLoad=true\n\n; Second active animation for GDI Kodiak\n[GAKODK_B]\nImage=GAKODK_B\nNormalized=yes\nLoopStart=0\nLoopEnd=22\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDemandLoad=true\n\n; Third active animation for GDI Kodiak\n[GAKODK_C]\nImage=GAKODK_C\nNormalized=yes\nLoopStart=0\nLoopEnd=15\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDemandLoad=true\n\n; Third damaged active animation for GDI Kodiak. This is a placeholder and just stops the non-damaged anim from playing\n[GAKODK_CD]\nImage=GAKODK_C\nNormalized=yes\nLoopStart=0\nLoopEnd=0\nLoopCount=0\nRate=220\nSurface=yes\nDemandLoad=true\n\n; Animation of tiberium leaving harvester and entering refinery\n[NAREFN_A]\nImage=NAREFN_A\nNormalized=yes\nLoopStart=0\nLoopEnd=5\nLoopCount=1\nRate=200\nSurface=yes\nNewTheater=yes\n\n; NAREFN_A but backwards\n[NAREFN_AR]\nImage=NAREFN_A\nNormalized=yes\nLoopStart=0\nLoopEnd=5\nLoopCount=1\nReverse=yes\nRate=200\nSurface=yes\n;NewTheater=yes\n\n; Active animation for refinery\n[NAREFN_C]\nImage=NAREFN_C\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n; Active animation for refinery. Fire ball.\n[NAREFN_B]\nImage=NAREFN_B\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nRate=350\nSurface=yes\nRandomLoopDelay=10,300\nNewTheater=yes\nDetailLevel=1\nShouldUseCellDrawer=false\n;Translucency=50\nTranslucent=yes\nUseNormalLight=yes\n\n; Active animation for helipad.\n[GAHPAD_A]\nImage=GAHPAD_A\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Damaged active animation for helipad.\n[GAHPAD_AD]\nImage=GAHPAD_A\nNormalized=yes\nLoopStart=8\nLoopEnd=16\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Active animation for Nod helipad.\n[NAHPAD_A]\nImage=NAHPAD_A\nNormalized=yes\nLoopStart=0\nLoopEnd=46\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Damaged active animation for Nod helipad.\n[NAHPAD_AD]\nImage=NAHPAD_A\nNormalized=yes\nStart=46\nLoopStart=46\nLoopEnd=92\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Repair bay active animation\n[GADEPT_A]\nImage=GADEPT_A\nNormalized=yes\nLoopStart=0\nLoopEnd=5\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Damaged repair bay active animation\n[GADEPT_AD]\nImage=GADEPT_A\nNormalized=yes\nStart=5\nLoopStart=5\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n; Repair bay active animation\n[GADEPT_B]\nImage=GADEPT_B\nNormalized=yes\nLoopStart=0\nLoopEnd=7\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Repair bay working animation\n[GADEPT_D]\nImage=GADEPT_D\nNormalized=yes\nLoopStart=0\nLoopEnd=6\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n; Damaged repair bay working animation\n[GADEPT_DD]\nImage=GADEPT_D\nNormalized=yes\nStart=7\nLoopStart=7\nLoopEnd=14\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\n\n; Repair bay arm extending.\n[GADEPT_C1]\nImage=GADEPT_C\nNormalized=yes\nStart=0\nEnd=5\nRate=400\nNewTheater=yes\nSurface=no\n\n; Repair bay arm working\n[GADEPT_C2]\nImage=GADEPT_C\nNormalized=yes\nStart=5\nLoopStart=5\nLoopEnd=11\nLoopCount=-1\nRate=400\nSurface=no\nNewTheater=yes\n\n; Repair bay arm retracting\n[GADEPT_C3]\nImage=GADEPT_C\nNormalized=yes\nStart=11\nEnd=16\nRate=400\nSurface=no\nNewTheater=yes\n\n[GATECH_A]\nImage=GATECH_A\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[GATECH_AD]\nImage=GATECH_A\nNormalized=yes\nStart=8\nLoopStart=8\nLoopEnd=16\nLoopCount=-1\nRate=190\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[NATECH_A]\nImage=NATECH_A\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[NATECH_AD]\nImage=NATECH_A\nNormalized=yes\nStart=10\nLoopStart=10\nLoopEnd=20\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n;[GADROP_A]\n;Image=GADROP_A\n;Normalized=yes\n;LoopStart=0\n;LoopEnd=20\n;LoopCount=-1\n;Rate=350\n;Surface=yes\n;NewTheater=yes\n\n;[GADROP_B]\n;Image=GADROP_B\n;Normalized=yes\n;LoopStart=0\n;LoopEnd=20\n;LoopCount=-1\n;Rate=350\n;Surface=yes\n;NewTheater=yes\n\n;[GADROP_BD]\n;Image=GADROP_B\n;Normalized=yes\n;Start=20\n;LoopStart=20\n;LoopEnd=40\n;LoopCount=-1\n;Rate=220\n;Surface=yes\n;NewTheater=yes\n\n[NAWAST_A]\nImage=NAWAST_A\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[NAWAST_AD]\nImage=NAWAST_A\nNormalized=yes\nStart=20\nLoopStart=20\nLoopEnd=40\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[NAWAST_B]\nImage=NAWAST_B\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[NAWAST_BD]\nImage=NAWAST_B\nNormalized=yes\nStart=8\nLoopStart=8\nLoopEnd=16\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[NAOBEL_A]\nImage=NAOBEL_A\nNormalized=yes\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n; Obelisk charging animation.\n[NAOBEL_B]\nImage=NAOBEL_B\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=12\nRate=0\nSurface=yes\nNewTheater=yes\n\n; Missile silo launch anim\n[NAMISL_A]\nImage=NAMISL_A\nNormalized=yes\nLoopStart=0\nLoopEnd=11\nLoopCount=1\nRate=350\nSurface=yes\nNewTheater=yes\nAltPalette=yes\n\n; Damaged missile silo launch anim\n[NAMISL_AD]\nImage=NAMISL_A\nNormalized=yes\nStart=11\nLoopStart=11\nLoopEnd=22\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\n\n[NAMISL_B]\nImage=NAMISL_B\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\n\n[NAMISL_BD]\nImage=NAMISL_B\nNormalized=yes\nStart=10\nLoopStart=10\nLoopEnd=20\nLoopCount=-1\nRate=220\n;NewTheater=yes\nSurface=yes\n\n[GAFIRE_B]\nImage=GAFIRE_B\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=500\nSurface=yes\nNewTheater=yes\n\n[GAFIRE_C]\nImage=GAFIRE_C\nNormalized=yes\nLoopStart=0\nLoopEnd=6\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\n\n[NAPOST_A]\nImage=NAPOST_A\nNormalized=yes\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[NAPOST_AD]\nImage=NAPOST_A\nNormalized=yes\nStart=12\nLoopStart=12\nLoopEnd=24\nLoopCount=-1\nRate=150\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[NAPOST_B]\nImage=NAPOST_B\nNormalized=yes\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=220\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n; Waterfall animation\n[WA01X]\nTheater=yes\nNormalized=yes\nLoopStart=1\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WA02X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WA03X]\nTheater=yes\nNormalized=yes\nLoopStart=1\t;was 0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WA04X]\nTheater=yes\nNormalized=yes\nLoopStart=1\t;was 0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WB01X]\nTheater=yes\nNormalized=yes\nLoopStart=1\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WB02X]\nTheater=yes\nNormalized=yes\nLoopStart=1\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WB03X]\nTheater=yes\nNormalized=yes\nLoopStart=1\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WB04X]\nTheater=yes\nNormalized=yes\nLoopStart=1\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n\n; Waterfall animation\n[WC01X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WC02X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WC03X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WC04X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n\n; Waterfall animation\n[WD01X]\nTheater=yes\nNormalized=yes\nLoopStart=1\t;was 0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WD02X]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WD03X]\nTheater=yes\nNormalized=yes\nLoopStart=1\t;was 0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Waterfall animation\n[WD04X]\nTheater=yes\nNormalized=yes\nLoopStart=1\t;was 0\nLoopEnd=8\nLoopCount=-1\nRate=220\nFlat=yes\nDetailLevel=2\nDemandLoad=true\nShouldUseCellDrawer=true\n\n; Crashed scrin fighter/UFO\n;[UFO]\n;Theater=yes\n;Normalized=yes\n;LoopStart=0\n;LoopEnd=0\n;LoopCount=-1\n;Rate=0\n;Flat=yes\n;DetailLevel=0\n;DemandLoad=true\n\n; Tiberium silo fill animation\n[GASILO_A]\nImage=GASILO_A\nNormalized=yes\nRate=0\nSurface=yes\nNewTheater=yes\n\n[GASILO_AD]\nImage=GASILO_A\nNormalized=yes\nStart=4\nRate=0\nSurface=yes\nNewTheater=yes\n\n; Tiberium silo active animation\n[GASILO_B]\nImage=GASILO_B\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[GASILO_BD]\nImage=GASILO_B\nNormalized=yes\nStart=16\nLoopStart=16\nLoopEnd=32\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[GASPOT_A]\nImage=GASPOT_A\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=1\n\n[GASPOT_AD]\nImage=GASPOT_A\nNormalized=yes\nStart=8\nLoopStart=8\nLoopEnd=16\nLoopCount=-1\nRate=220\nSurface=yes\n;NewTheater=yes\nDetailLevel=1\n\n[CTDAM_A]\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\n;TerrainPalette=yes\n;NewTheater=yes\n;DemandLoad=true\n\n[CTDAM_AD]\nImage=CTDAM_A\nNormalized=yes\nStart=10\nLoopStart=10\nLoopEnd=20\nLoopCount=-1\nRate=220\n;TerrainPalette=yes\n;NewTheater=yes\n;DemandLoad=true\n\n[CTDAM_B]\nNormalized=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=350\nSurface=yes\n;TerrainPalette=yes\n;NewTheater=yes\n;DemandLoad=true\n\n[CTDAM_BD]\nImage=CTDAM_B\nNormalized=yes\nStart=8\nLoopStart=8\nLoopEnd=16\nLoopCount=-1\nRate=350\nSurface=yes\n;TerrainPalette=yes\n;NewTheater=yes\n;DemandLoad=true\n\n\n; Tunnel roof\n[TUNTOP01]\nTheater=yes\nNormalized=yes\nSurface=yes\nYSortAdjust=1000\nLoopStart=0\nLoopEnd=0\nLoopCount=-1\nRate=0\nFlat=yes\nDetailLevel=0\nDemandLoad=true\nShouldFogRemove=false\n\n; Tunnel roof\n[TUNTOP02]\nTheater=yes\nSurface=yes\nYSortAdjust=1000\nNormalized=yes\nLoopStart=0\nLoopEnd=0\nLoopCount=-1\nRate=0\nFlat=yes\nDetailLevel=0\nDemandLoad=true\nShouldFogRemove=false\n\n; Tunnel roof\n[TUNTOP03]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=0\nLoopCount=-1\nRate=0\nFlat=yes\nDetailLevel=0\nDemandLoad=true\nShouldFogRemove=false\n\n; Tunnel roof\n[TUNTOP04]\nTheater=yes\nNormalized=yes\nLoopStart=0\nLoopEnd=0\nLoopCount=-1\nRate=0\nFlat=yes\nDetailLevel=0\nDemandLoad=true\nShouldFogRemove=false\n\n; Larger meteor\n[METLARGE]\nElasticity=0.0\nMaxXYVel=100.0\nMinZVel=-50.0\nExpireAnim=TWLT070\nDamage=5000000\nDamageRadius=300\nWarhead=Meteorite\nIsMeteor=true\nSpawns=METDEBRI\nSpawnCount=5\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRandomRate=220,500\nDetailLevel=0\nTrailerAnim=SMOKEY2\nTrailerSeperation=1\nReport=METEOR1\n\n; Small meteor\n[METSMALL]\nElasticity=0.0\nMinZVel=-50.0\nMaxXYVel=100.0\nExpireAnim=TWLT100\nDamage=5000000\nDamageRadius=300\nWarhead=Meteorite\nIsMeteor=true\nIsTiberium=true\nSpawns=METDEBRI\nSpawnCount=7\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRandomRate=220,500\nDetailLevel=0\nTrailerAnim=METSTRAL\nTrailerSeperation=1\nReport=METEOR2\n\n; Meteor impact debris\n[METDEBRI]\nElasticity=0.0\nMinZVel=40.0\nMaxXYVel=18.0\nExpireAnim=TWLT070\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=220,500\nBouncer=yes\n;TiberiumRadius=1\nTiberiumSpawnType=TIB01\nReport=METHIT1\n\n;Meteor smoke trail\n[METSTRAL]\nLoopStart=0\nLoopEnd=8\nLoopCount=1\nRate=600\nDetailLevel=1\nNext=SMOKEY\n\n; Meteor trail\n[METLTRAL]\nLoopStart=0\nLoopEnd=8\nLoopCount=1\nRate=600\nDetailLevel=1\nNext=SMOKEY\n\n[CRYSTAL1]\nElasticity=0.0\nMinZVel=40.0\nMaxXYVel=18.0\nExpireAnim=TWLT026\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=220,500\nBouncer=yes\nTheater=yes\nTiberiumSpreadRadius=0\nTiberiumSpawnType=TIB2_01\n\n[CRYSTAL2]\nElasticity=0.0\nMinZVel=40.0\nMaxXYVel=18.0\nExpireAnim=TWLT026\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=220,500\nBouncer=yes\nTheater=yes\nTiberiumSpreadRadius=0\nTiberiumSpawnType=TIB2_01\n\n[CRYSTAL3]\nElasticity=0.0\nMinZVel=40.0\nMaxXYVel=18.0\nExpireAnim=TWLT026\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=220,500\nBouncer=yes\nTheater=yes\nTiberiumSpreadRadius=0\nTiberiumSpawnType=TIB2_01\n\n[CRYSTAL4]\nElasticity=0.0\nMinZVel=40.0\nMaxXYVel=18.0\nExpireAnim=TWLT026\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\nLoopStart=0\nLoopEnd=14\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=220,500\nBouncer=yes\nTheater=yes\nTiberiumSpreadRadius=0\nTiberiumSpawnType=TIB2_01\nAnimLow=0\nAnimHigh=14\nVoxel=no\n\n[BIGBLUE]\nImage=BIGBLUE3\nLoopStart=0\nLoopEnd=9\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=150,250\n;Theater=yes\nIsAnimatedTiberium=yes\nSurface=yes\nYDrawOffset=-52\nAltPalette=yes\nUseNormalLight=yes\n\n[BIGBLUE3]\n;Theater=yes\nFoundation=1x1\nAltPalette=yes\nLoopStart=0\nLoopEnd=9\nLoopCount=-1\nRate=500\nDetailLevel=0\nRandomRate=150,250\nSurface=yes\nYDrawOffset=-16\nUseNormalLight=yes\n\n[FLAMEGUY]\nIsFlamingGuy=true\nRunningFrames=6\nLoopCount=1\nRate=500\n\n; This is a tricky one. It's an animation AND a projectile all in the same section\n; The anim stuff is first then the projectile stuff.\n[PULSBALL]\nStart=0\nEnd=8\nLoopCount=1\nRate=220\nDetailLevel=0\nAnimPalette=yes\nAnimLow = 8\nAnimHigh = 22\nAnimRate = 1\n\n; Deployable sensor array\n[GADPSA_A]\nNormalized=yes\nRate=220\nSurface=yes\nNewTheater=yes\nLoopCount=-1\n\n; Deployable ICBM launcher\n[GAICBM_A]\nRate=0\nSurface=yes\nNewTheater=yes\nLoopCount=-1\n\n; Civilian array\n[CAARAY_A]\nImage=CAARAY_A\nNormalized=yes\nLoopStart=0\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=0\nLoopEnd=16\nDemandLoad=true\n\n[CAARAY_B]\nImage=CAARAY_B\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nDemandLoad=true\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=0\n\n[CAARAY_C]\nImage=CAARAY_C\nNormalized=yes\nLoopStart=0\nLoopEnd=16\nDemandLoad=true\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=2\n\n[CAARAY_CD]\nImage=CAARAY_C\nNormalized=yes\nLoopStart=16\nLoopEnd=32\nDemandLoad=true\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=2\n\n[CAARAY_D]\nImage=CAARAY_D\nNormalized=yes\nLoopStart=0\nLoopEnd=12\nDemandLoad=true\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=2\n\n[CAARAY_DD]\nImage=CAARAY_D\nNormalized=yes\nStart=12\nLoopStart=12\nLoopEnd=24\nDemandLoad=true\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=yes\nDetailLevel=2\n\n[CARYLAND]\nImage=PODRING\nNormalized=no\nRate=900\nSurface=yes\nDetailLevel=1\nTranslucent=yes\nFlat=yes\n;NormalZAdjust=1\nYSortAdjust=-2000\n\n[DROPLAND]\nImage=PODRING\nNormalized=no\nRate=180\nSurface=yes\nDetailLevel=1\nTranslucent=yes\nFlat=yes\n;NormalZAdjust=1\nYSortAdjust=-200\n\n;============================================================================\n; FIRESTORM ADDITIONS & CHANGES\n;============================================================================\n\n[FONA01]\nTheater=yes\nFoundation=1x1\n\n[FONA02]\nTheater=yes\nFoundation=1x1\n\n[FONA03]\nTheater=yes\nFoundation=1x1\n\n[FONA04]\nTheater=yes\nFoundation=1x1\n\n[FONA05]\nTheater=yes\nFoundation=1x1\n\n[FONA06]\nTheater=yes\nFoundation=1x1\n\n[FONA07]\nTheater=yes\nFoundation=1x1\n\n[FONA08]\nTheater=yes\nFoundation=1x1\n\n[FONA09]\nTheater=yes\nFoundation=1x1\n\n[FONA10]\nTheater=yes\nFoundation=1x1\n\n[FONA11]\nTheater=yes\nFoundation=1x1\n\n[FONA12]\nTheater=yes\nFoundation=1x1\n\n[FONA13]\nTheater=yes\nFoundation=1x1\n\n[FONA14]\nTheater=yes\nFoundation=1x1\n\n[FONA15]\nTheater=yes\nFoundation=1x1\n\n; Infantry struggle under web\n[WEBGUY]\nNormalized=true\nAltPalette=false\nFlat=yes\nSurface=yes\nRandomLoopDelay=10,300\n\n\n[MEMPFX]\nNormalized=yes\nSurface=yes\nTranslucent=yes\nUseNormalLight=yes\n\n\n; Deployed limpet mine\n[DLIMPET]\nFoundation=1x1\nHeight=1\nBuildup=DLIMPMK\nAnimActive=0,10,3\nActiveAnim=DLIMP_A\n\n[DLIMP_A]\nImage=DLIMP_A\nNormalized=yes\nLoopStart=0\nLoopEnd=10\nLoopCount=-1\nRate=350\nSurface=yes\nNewTheater=no\nDetailLevel=1\n;DemandLoad=true\n\n\n; Kodiak Crash\n[C_KODIAK]\nFoundation=3x3\nTerrainPalette=yes\nExtraDamageStage=false\n;DemandLoad=true\nHeight=3\nAnimActive=0,20,3\nActiveAnim=K_LIGHT1\nActiveAnimX=-23\nActiveAnimY=0\nActiveAnimZAdjust=-100\nActiveAnimTwo=K_LIGHT2\nActiveAnimTwoX=144\nActiveAnimTwoY=144\nActiveAnimTwoZAdjust=-100\n\n[K_LIGHT1]\nImage=K_LIGHT1\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nRate=350\nSurface=no\t;yes\nNewTheater=no\nDetailLevel=1\n;DemandLoad=true\n\n[K_LIGHT2]\nImage=K_LIGHT2\nNormalized=yes\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nRate=350\nSurface=no\t;yes\nNewTheater=no\nDetailLevel=1\n;DemandLoad=true\n\n\n; Mobile EMP\n[M_EMP]\nCameo=MEMPICON\nRemapable=yes\nVoxel=yes\n\n\n; Mobile Stealth Generator\n[SGEN]\nCameo=MSTLICON\nRemapable=yes\nVoxel=yes\n\n\n; Mobile Weapons Factory\n[MWAR_NOD]\nCameo=MWARICON\nRemapable=yes\nVoxel=yes\n\n\n; Juggernaught\n[JUGGER]\nCameo=JUGGICON\nVoxel=no\nRemapable=yes\nWalkFrames=15\nStandingFrames=0\nFacings=8\n\n\n[LIMPED]\nCameo=LIMPICON\n\n\n; Mobile Weapons Factory\n[MWAR]\nRemapable=yes\nFoundation=4x3\nHeight=2\nNormalZAdjust=-10\nAnimActive=0,1,0\nBuildup=MWARMK\n;DemandLoadBuildup=true\n;FreeBuildup=true\nDeployingAnim=MWAR_2\nDoorAnim=MWAR_D\nDoorStages=12\nUnderDoorAnim=MWAR_1\n;NewTheater=yes\nBibShape=MWARBB\nActiveAnim=MWAR_A\nActiveAnimZAdjust=-119\nActiveAnimTwo=MWAR_B\nActiveAnimTwoZAdjust=-119\nActiveAnimThree=MWAR_C\nActiveAnimThreeZAdjust=-119\n\n; Mobile Weapons Factory (Overlay A)\n[MWAR_A]\nNormalized=yes\n;NewTheater=yes\nLoopStart=0\nLoopEnd=5\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n; Mobile Weapons Factory (Overlay B)\n[MWAR_B]\nNormalized=yes\n;NewTheater=yes\nLoopStart=0\nLoopEnd=12\nLoopCount=-1\nRate=400\nSurface=yes\nDetailLevel=1\n\n; Mobile Weapons Factory (Overlay C)\n[MWAR_C]\nNormalized=yes\n;NewTheater=yes\nLoopStart=0\nLoopEnd=8\nLoopCount=-1\nRate=800\nSurface=yes\nDetailLevel=1\n\n\n; Deployed Juggernaut\n[DJUGG]\nNormalized=yes\nRemapable=yes\nFoundation=1x1\nBuildup=DJUGGMK\nHeight=1\nPBarrelLength=224\nPrimaryFireFLH=0,0,64\nTurretNotExportedOnGround=yes\n\n\n; Deployed Mobile Stealth Generator\n[MSTL]\nRemapable=yes\nNormalized=yes\nFoundation=1x1\nBuildup=MSTLMK\n;DemandLoadBuildup=true\nHeight=1\n;FreeBuildup=true\nActiveAnim=MSTL_A\nExtraLight=-100\n\n; Deployed Mobile Stealth Generator (Overlay A)\n[MSTL_A]\nNormalized=yes\nRate=220\nSurface=yes\nLoopCount=-1\n\n\n[DEFENDER]\nWalkFrames=8\nFiringFrames=12\nVoxel=no\nRemapable=no\nPrimaryFireFLH=200,-200,450\nSecondaryFireFLH=200,200,450\nFiringSyncFrame1=8\nFiringSyncFrame2=3\nStartStandFrame=0\nStartWalkFrame=8\nStartFiringFrame=72\nFacings=8\n\n\n; Deployed Core Defender\n[DEFD]\nNormalized=yes\nRemapable=yes\nFoundation=2x2\nHeight=1\nBuildup=DEFDMK\n;DemandLoadBuildup=true\n\n\n; Core defender explosion\n[DEFD_EXP]\nNormalized=yes\nRate=350\nReport=EXPNEW05\nNext=TWLT100\n\n\n; Cabals Core\n[CORE]\nFoundation=3x3\nHeight=3\nBuildup=COREMK\n;DemandLoadBuildup=true\nExtraDamageStage=yes\nActiveAnim=CORE_A\nActiveAnimDamaged=CORE_AD\nActiveAnimZAdjust=-100\nActiveAnimTwo=CORE_B\nActiveAnimTwoDamaged=CORE_BD\nActiveAnimTwoZAdjust=-100;-25\nActiveAnimThree=CORE_C\nActiveAnimThreeDamaged=CORE_CD\nActiveAnimThreeZAdjust=-100;-50\n\n[CORE_A]\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=60\nLoopCount=-1\nSurface=yes\nNewTheater=no\nDetailLevel=1\nRate=200\n\n[CORE_AD]\nImage=CORE_A\nNormalized=yes\nStart=60\nLoopStart=60\nLoopEnd=120\nLoopCount=-1\nNewTheater=no\nSurface=yes\nDetailLevel=1\nRate=200\n\n[CORE_B]\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=20\nLoopCount=-1\nSurface=yes\nNewTheater=no\nDetailLevel=1\nRate=200\n\n[CORE_BD]\nImage=CORE_B\nNormalized=yes\nStart=20\nLoopStart=20\nLoopEnd=40\nLoopCount=-1\nNewTheater=no\nSurface=yes\nDetailLevel=1\nRate=200\n\n[CORE_C]\nNormalized=yes\nStart=0\nLoopStart=0\nLoopEnd=30\nLoopCount=-1\nSurface=yes\nNewTheater=no\nDetailLevel=1\nRate=200\n\n[CORE_CD]\nImage=CORE_C\nNormalized=yes\nStart=30\nLoopStart=30\nLoopEnd=60\nLoopCount=-1\nNewTheater=no\nSurface=yes\nDetailLevel=1\nRate=200\n\n\n; Cyborg reaper\n[REAPER]\nCameo=REAPICON\nFacings=8\nWalkFrames=12\nStandingFrames=1\nFiringFrames=0\n;DeathFrames=13\n;DeathFrameRate=3\nStartWalkFrame=8\nStartStandFrame=0\n;StartDeathFrame=104\n;MaxDeathCounter=64\nPrimaryFireFLH=50,110,100\nSecondaryFireFLH=0,0,230\n\n[REAPRDIE]\nNormalized=yes\nEnd=13\nRate=350\nAltPalette=yes\nReport=SPIDDIE1\nNext=S_TUMU60\t;TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\n\n\n[OBL1]\nRemapable=yes\nFoundation=2x2\nChargeAnim=yes\n;Buildup=\nHeight=3\nDemandLoadBuildup=true\nFreeBuildup=true\nPrimaryFirePixelOffset=2,-38\n;NewTheater=yes\n;AnimActive=0,24,3\nActiveAnim=OBL1_A\nActiveAnimDamaged=OBL1_AD\nActiveAnimZAdjust=-100\nActiveAnimTwo=OBL1_B\nActiveAnimTwoDamaged=OBL1_BD\nActiveAnimTwoZAdjust=-100\n\n[OBL1_A]\nImage=OBL1_A\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL1_AD]\nImage=OBL1_A\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL1_B]\nImage=OBL1_B\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL1_BD]\nImage=OBL1_B\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL1_C]\nImage=OBL1_C\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL1_CD]\nImage=OBL1_C\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n\n[OBL2]\nNormalized=yes\nRemapable=yes\nFoundation=1x2\nChargeAnim=yes\n;Buildup=\nHeight=4\nDemandLoadBuildup=true\nFreeBuildup=true\nPrimaryFirePixelOffset=-20,-76\n;AnimActive=0,24,3\nActiveAnim=OBL2_A\nActiveAnimDamaged=OBL2_AD\nActiveAnimZAdjust=-100\nActiveAnimTwo=OBL2_B\nActiveAnimTwoDamaged=OBL2_BD\nActiveAnimTwoZAdjust=-3\n\n[OBL2_A]\nImage=OBL2_A\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL2_AD]\nImage=OBL2_A\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL2_B]\nImage=OBL2_B\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL2_BD]\nImage=OBL2_B\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL2_C]\nImage=OBL2_C\nNormalized=yes\nSurface=yes\nStart=0\nLoopStart=0\nLoopEnd=24\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n[OBL2_CD]\nImage=OBL2_C\nNormalized=yes\nSurface=yes\nStart=24\nLoopStart=24\nLoopEnd=48\nLoopCount=-1\nRate=200\nDetailLevel=1\n\n; *** Movies ***\n; Each of the movies allowed in the game will be listed\n; here.\n[Movies]\n00=CAP_TRAT\n01=COUP\n02=VEGAWIN\n03=DISKDEST\n04=INTRO\n05=GDI_M02\n06=GDI_M03\n07=GDI_M04\n08=GDI_M05\n09=GDI_M06\n10=GDI_M07\n11=GDI_M08\n12=GDI_M09A\n13=GDI_M09B\n14=GDI_M09C\n15=GDI_M10A\n16=GDIM09CW\n17=GDI_M11\n18=GDI_M12A\n19=HIDESEEK\n20=ICESKATE\n21=MECHATAK\n22=EVA\n23=NOD_M02\n24=NOD_M03\n25=NOD_M04\n26=NOD_M06\n27=NOWCNOT\n28=ORCASTRK\n29=PODASSLT\n30=RETRBTN\n31=TENEVICT\n32=TRAINROB\n33=NOD06ABW\n34=EMPULSE\n35=NOD_M09\n36=STARTUP\n37=ICBMLNCH\n38=BEACHEAD\n39=GDI_FINL\n40=NOD_M05\n41=GENNODL1\n42=GDIM09D1\n43=GDI01_SB\n44=GDI02_SB\n45=GDI03_SB\n46=NOD_M07\n47=NOD_M08\n48=NOD_M10\n49=NOD_M11\n50=NOD_M12\n51=NOD_FINL\n52=NOD01_SB\n53=NOD02_SB\n54=GENWIN01\n55=UFOGUARD\n56=WWLOGO\n57=KILL_GDI\n58=KILLMECH\n59=UNSTPBLE\n60=N_LOGO_W\n61=N_LOGO_L\n62=NOD_FLAG\n63=GDI_LOGO\n64=GDI_FLAG\n65=DAMBREAK\n66=FSGDIM02 ; Firestorm movies start here\n67=FSGDIM03\n68=FSGDIM07\n69=FSNODM02\n70=FSNODM06\n71=FS_TITLE\n72=FSNODM01\n73=FSNODM03\n74=FSNODM04\n75=FSNODM07\n76=FSNODM09\n77=FSNODM05\n78=FSNODM08\n79=MEKATAK2\n80=FSGDIM04\n81=FSGDIM05\n82=FSGDIM06\n83=FSGDIM08\n84=FSGDIM09\n85=FSGDIFNL\n86=FSGDIINT\n87=FS_SB01\n88=TS_TITLE\n89=FSNODFNL\n90=INTRON\n91=MOBSTLTH\n92=RAGDOLL\n93=REAPBOMB\n"
  },
  {
    "path": "DXMainClient/Resources/INI/artfs.ini",
    "content": ";============================================================================\n; FIRESTORM ADDITIONS & CHANGES\n;============================================================================\n\n[GAWALL]\nDamageLevels=2\n\n[NAWALL]\nDamageLevels=2\n"
  },
  {
    "path": "DXMainClient/Resources/INI/day.ini",
    "content": "[VariableNames]\n0=Get Dark,1\n1=Get Light,0\n\n[Triggers]\n09565DC0=Neutral,<none>,Start Cycle,0,1,1,1,0\n09565750=Neutral,<none>,Nightfall,0,1,1,1,0\n09565310=Neutral,<none>,Daybreak,0,1,1,1,0\n09F31930=Neutral,<none>,Light On,0,1,1,1,0\n09F31410=Neutral,09F31930,Light Off,0,1,1,1,0\n\n[Events]\n09565DC0=1,47,0,180\n09565750=2,36,0,0,13,0,1800\n09565310=2,36,0,1,13,0,1800\n09F31930=1,45,0,60\n09F31410=1,46,0,70\n\n[Actions]\n09565DC0=1,56,0,0,0,0,0,0,A\n09565750=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\n09565310=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\n09F31930=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\n09F31410=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\n\n[Tags]\n09565870=0,Start Cycle,09565DC0\n095653C0=2,Nightfall,09565750\n09565060=2,Daybreak,09565310\n09F314C0=2,Light On/Off,09F31410\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/dusk.ini",
    "content": "[VariableNames]\n0=Get Dark,1\n1=Get Light,0\n\n[Triggers]\n09565DC0=Neutral,<none>,Go to Night,0,1,1,1,0\n09565750=Neutral,<none>,Nightfall,0,1,1,1,0\n09565310=Neutral,<none>,Daybreak,0,1,1,1,0\n09F31930=Neutral,<none>,Light On,0,1,1,1,0\n09F31410=Neutral,09F31930,Light Off,0,1,1,1,0\n\n[Events]\n09565DC0=1,47,0,180\n09565750=2,13,0,1800,36,0,0\n09565310=2,13,0,1800,36,0,1\n09F31930=1,45,0,60\n09F31410=1,46,0,70\n\n[Actions]\n09565DC0=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\n09565750=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\n09565310=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\n09F31930=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\n09F31410=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\n\n[Tags]\n09565870=0,Go to Night,09565DC0\n095653C0=2,Nightfall,09565750\n09565060=2,Daybreak,09565310\n09F314C0=2,Light On/Off,09F31410\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/firestrm.ini",
    "content": "[General]\nName=Tiberian Sun - Firestorm\nDropPodInfantryMinimum=5  ;was 3\nDropPodInfantryMaximum=8  ;was 5\nBallisticScatter=2.0  ;was 1.5\nSurvivorRate=.1   ; was .4\nSurvivorDivisor=100\n\n; veteran factors updated\nVeteranRatio=5.0        ; must destroy this multiple of self-value to become a veteran [per level]\nVeteranCombat=.50       ; combat BONUS factor when unit is a veteran\nVeteranSpeed=.30        ; speed BONUS factor when unit is a veteran\nVeteranSight=0.0        ; sight range BONUS when unit is a veteran\nVeteranArmor=.50        ; armor BONUS when unit is a veteran\nVeteranROF=.30          ; rate of fire BONUS when unit is a veteran\nVeteranCap=2            ; maximum veteran level that can be obtained\nInitialVeteran=no       ; Do initial forces start as veterans?\n\n\n[JumpjetControls]\nCloakDetectionRadius=3\n\n\n;============================================================================\n; VEHICLES / UNITS\n;============================================================================\n\n[REAPER]\nTechLevel=6\nAllowedToStartInMultiplayer=yes\n\n[JUGG]\nTechLevel=6\nAllowedToStartInMultiplayer=yes\n\n[LIMPET]\nTechLevel=3\n\n[MOBILEMP]\nTechLevel=6\nIsMobileEMP=true\t;resets if not redefined\n\n[SGEN]\nTechLevel=9\n\n[MOBWARG]\nTechLevel=10\n\n[MOBWARN]\nTechLevel=10\n\n[SONIC]\nSpeed=5\nEliteAbilities=FASTER\nAccelerates=false\n\n[STNK]\nStrength=200 ; w180\nSight=7\n\n[TRUCKA]\nCrewed=no\n\n[TRUCKB]\nCrewed=no\n\n\n;============================================================================\n; AIRCRAFT\n;============================================================================\n\n[SCRIN]\nCost=1250  ;was 1500\n\n\n[APACHE]\nCost=800  ;was 1000\n\n\n;============================================================================\n; INFANTRY\n;============================================================================\n\n[JUMPJET]\nSpeed=8\n\n[E2]\nCollateralDamageCoefficient=.33\n\n\n\n;============================================================================\n; BUILDINGS\n;============================================================================\n[GAPLUG4]\nTechLevel=10\nAIBuildThis=yes\n\n[CA0016]\nName=D's Dog House\n\n\n;============================================================================\n; WEAPON / PROJECTILE\n;============================================================================\n[DropGun]\nDamage=1 ;was 50\n\n;Fix to make Artillery less accurate\n[Ballistic]\nHigh=yes\nImage=120MM\nArcing=true\nBouncy=yes\nElasticity=0.0\n\n[SlimeAttack]\nRange=2.0\n\n[Grenade]\nROF=80  ;was 60\n\n[Bomb]\nRange=3 ;was 5\n\n\n;============================================================================\n; TERRIAN OVERLAYS\n;============================================================================\n[TerrainTypes]\n1=GAWALL\n2=NAWALL\n\n[GAWALL]\nStrength=225\n\n[NAWALL]\nStrength=225\n\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/ion.ini",
    "content": "[General]\nIonLightningFrequency=10\nIonLightningRandomness=90\nIonLightningDamage=200\nIonStormWarning=33\n\n[Lighting]\nIonAmbient=0.500000\nIonRed=1.620000\nIonGreen=1.250000\nIonBlue=0.340000\nIonGround=0.000000\nIonLevel=0.000000\n\n\n[Triggers]\n07709990=Neutral,<none>,Ion Storm,0,1,1,1,0\n\n[Events]\n07709990=1,51,0,1800\n\n[Actions]\n07709990=1,44,0,240,0,0,0,0,A\n\n[Tags]\n07709670=2,Ion Storm,07709990\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/keyboard.ini",
    "content": "[Hotkey]\nChatToAllies=8\nCenterView=12\nChatToAll=13\nShowHelp=20\nOptions=27\nToggleRadar=9\nCenterOnRadarEvent=32\nRightSidebarUp=33\nRightSidebarDown=34\nLeftSidebarDown=35\nLeftSidebarUp=36\nTeamSelect_10=48\nTeamSelect_1=49\nTeamSelect_2=50\nTeamSelect_3=51\nTeamSelect_4=52\nTeamSelect_5=53\nTeamSelect_6=54\nTeamSelect_7=55\nTeamSelect_8=56\nTeamSelect_9=57\nToggleAlliance=65\nPreviousObject=66\nDeployObject=68\nSelectView=69\nFollow=70\nGuardObject=71\nCenterBase=72\nNextObject=78\nTogglePower=80\nToggleRepair=82\nStopObject=83\nSelectType=84\nWaypointMode=87\nScatterObject=88\nPlaceBuilding=90\nPageUser=106\nDeleteWaypoint=110\nView1=120\nView2=121\nView3=122\nView4=123\nToggleInfoPanel=192\nTeamAddSelect_10=304\nTeamAddSelect_1=305\nTeamAddSelect_2=306\nTeamAddSelect_3=307\nTeamAddSelect_4=308\nTeamAddSelect_5=309\nTeamAddSelect_6=310\nTeamAddSelect_7=311\nTeamAddSelect_8=312\nTeamAddSelect_9=313\nToggleSell=594\nRepeatBuilding=602\nTeamCreate_10=560\nTeamCreate_1=561\nTeamCreate_2=562\nTeamCreate_3=563\nTeamCreate_4=564\nTeamCreate_5=565\nTeamCreate_6=566\nTeamCreate_7=567\nTeamCreate_8=568\nTeamCreate_9=569\nScreenCapture=579\nSetView1=632\nSetView2=633\nSetView3=634\nSetView4=635\nTeamCenter_10=1072\nTeamCenter_1=1073\nTeamCenter_2=1074\nTeamCenter_3=1075\nTeamCenter_4=1076\nTeamCenter_5=1077\nTeamCenter_6=1078\nTeamCenter_7=1079\nTeamCenter_8=1080\nTeamCenter_9=1081\nScrollWest=2085\nScrollNorth=2086\nScrollEast=2087\nScrollSouth=2088\n;ForceWin=1111\n;BailOut=1112\n;SidebarPageUp=2085\n;SidebarUp=2086\n;SidebarPageDown=2087\n;SidebarDown=2088\n"
  },
  {
    "path": "DXMainClient/Resources/INI/morning.ini",
    "content": "[VariableNames]\n0=Get Dark,1\n1=Get Light,0\n\n[Triggers]\n09565DC0=Neutral,<none>,Go to Day,0,1,1,1,0\n09565750=Neutral,<none>,Nightfall,0,1,1,1,0\n09565310=Neutral,<none>,Daybreak,0,1,1,1,0\n09F31930=Neutral,<none>,Light On,0,1,1,1,0\n09F31410=Neutral,09F31930,Light Off,0,1,1,1,0\n\n[Events]\n09565DC0=1,47,0,180\n09565750=2,36,0,0,13,0,1800\n09565310=2,36,0,1,13,0,1800\n09F31930=1,45,0,60\n09F31410=1,46,0,70\n\n[Actions]\n09565DC0=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\n09565750=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\n09565310=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\n09F31930=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\n09F31410=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\n\n[Tags]\n09565870=0,Go to Day,09565DC0\n095653C0=2,Nightfall,09565750\n09565060=2,Daybreak,09565310\n09F314C0=2,Light On/Off,09F31410\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/night.ini",
    "content": "[VariableNames]\n0=Get Dark,1\n1=Get Light,0\n\n[Triggers]\n09565DC0=Neutral,<none>,Start Cycle,0,1,1,1,0\n09565750=Neutral,<none>,Nightfall,0,1,1,1,0\n09565310=Neutral,<none>,Daybreak,0,1,1,1,0\n09F31930=Neutral,<none>,Light On,0,1,1,1,0\n09F31410=Neutral,09F31930,Light Off,0,1,1,1,0\n\n[Events]\n09565DC0=1,47,0,180\n09565750=2,13,0,1800,36,0,0\n09565310=2,13,0,1800,36,0,1\n09F31930=1,45,0,60\n09F31410=1,46,0,70\n\n[Actions]\n09565DC0=1,56,0,1,0,0,0,0,A\n09565750=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\n09565310=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\n09F31930=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\n09F31410=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\n\n[Tags]\n09565870=0,Start Cycle,09565DC0\n095653C0=2,Nightfall,09565750\n09565060=2,Daybreak,09565310\n09F314C0=2,Light On/Off,09F31410\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/rules.ini",
    "content": "; RULE*.INI - update8\n; *** Tiberian Sun Rules ***\n; If placed in game directory, it will override built in values. Values to be used as multipliers\n; or percentages can be specified as either a simple floating point number (embed \".\") or as a\n; conventional percentage number (append \"%\"). Values used as distances or time delays\n; are specified as simple floating point number. Distance values are expressed in cells. Time\n; values are expressed in minutes.\n; If multiple rules files are present, the Name field is used to identify between them.\n\n[General]\nName=Tiberian Sun -- Official Rules of Engagement\n\n; veteran factors\nVeteranRatio=10.0       ; must destroy this multiple of self-value to become a veteran [per level]\nVeteranCombat=.25       ; combat BONUS factor when unit is a veteran\nVeteranSpeed=.30        ; speed BONUS factor when unit is a veteran\nVeteranSight=0.0        ; sight range BONUS when unit is a veteran\nVeteranArmor=.25        ; armor BONUS when unit is a veteran\nVeteranROF=.20          ; rate of fire BONUS when unit is a veteran\nVeteranCap=2            ; maximum veteran level that can be obtained\nInitialVeteran=no       ; Do initial forces start as veterans?\n\n; repair and refit\nRefundPercent=50%       ; percent of original cost to refund when building/unit is sold\nReloadRate=.5           ; minutes to reload each ammo point for aircraft or helicopters\nRepairPercent=20%       ; percent cost to fully repair as ratio of full cost\nRepairRate=.016         ; minutes between applying repair step\nRepairStep=8            ; hit points to heal per repair 'tick'\nURepairRate=.016        ; [units only] minutes between applying repair step\nUnitSelfHealRepairStep=1; amount of HP to increase unit's health with for every step while self-healing (def=1)\nIRepairRate=.001        ; [infantry only] minutes between applying repair step\nIRepairStep=1           ; [infantry only] hit points to heal per repair 'tick' for infantry\nTiberiumHeal=.010       ; minutes between applying Tiberium healing [for those units that heal in Tiberium]\n\n; income and production\n;BailCount=28            ; number of 'bails' carried by a harvester\nBuildSpeed=.8           ; general build speed [time (in minutes) to produce a 1000 credit cost item]\nBuildupTime=.06         ; average minutes that building build-up animation runs\nGrowthRate=5            ; minutes between ore (Tiberium) growth\nTiberiumGrows=yes       ; Does ore grow denser over time?\nTiberiumSpreads=yes     ; Does ore spread into adjacent areas?\nSeparateAircraft=yes    ; Is first helicopter to be purchased separately from helipad?\nSurvivorRate=.4         ; fraction of building cost to be converted to survivors when sold\nSurvivorDivisor=100     ; the divisor into the survivor rate value to determine the number of survivors\nPlacementDelay=.05      ; delay before retrying produced object deploy if temporary blockage detected\nWeedCapacity=56         ; Amount of weed that needs to be harvested by a house in order to build the chem missile\n;HarvesterDumpRate=0.0\n;HarvesterLoadRate=1.0\n\n; computer and movement controls\nCurleyShuffle=yes       ; Should helicopter shuffle position between shots [as in C&C]?\nBaseBias=2              ; multiplier to threat target value when enemy is close to friendly base\nBaseDefenseDelay=.25    ; minutes delay between sending response teams to deal with base threat\nCloseEnough=2.25        ; If distance to destination less than this, then abort movement if otherwise blocked.\nDamageDelay=1           ; minutes between applying trivial structure damage when low on power\nGameSpeedBias=1         ; multiplier to overall game object movement speed\nStray=2.0               ; radius distance (cells) that team members may stray without causing regroup action\nCloakDelay=.02          ; forced delay that subs will remain on surface before allowing to submerge\nSuspendDelay=2          ; minutes that suspended teams will remain suspended\nSuspendPriority=1       ; teams with less than this priority will suspend during base defense ops\nFlightLevel=600         ; typical flight level for aircraft [above ground level]\nMissileSpeedVar=.25     ; speed flucuation percentage that guided missiles have\nMissileROTVar=.25       ; rate of turn fluctuation percentage that guided missiles have\n\nTeamDelays=2250,2700,3600              ; interval between checking for and creating teams, by difficulty level\nAIHateDelays=5400,4500,4050             ; delay in frames before the computer chooses an enemy, by difficulty level\nAIAlternateProductionCreditCutoff=3000 ; when the AI house has less credits than this it will begin\n                                        ; to spend money more conservatively\nNodAIBuildsWalls=no\nAIBuildsWalls=no\nMultiplayerAICM=250,200,100\nHealScanRadius=10       ; how far should medic-type units scan for targets?  Used to override the range\n                        ; of these units, because they need to have very short ranges\nFillEarliestTeamProbability=100,80,60 ; (by difficulty level, from hardest to easiest)\nMinimumAIDefensiveTeams=4,3,2    ; (by difficulty level, from hardest to easiest)\nMaximumAIDefensiveTeams=6,5,4\t ; \"                                            \"\nTotalAITeamCap=14,12,10   ; (by difficulty level, from hardest to easiest)\nUseMinDefenseRule=yes\nDissolveUnfilledTeamDelay=9000\t; how long to wait before dissolving an ai trigger team that has no members (multiplay only)\nLargeVisceroid=VISC_LRG  ; when two small visceroids combine they turn into this\nSmallVisceroid=VISC_SML  ; when infantry transmorgifies into a visceroid\n\n; controls how the computer AI scores potential ion cannon targets\n; the first value is for hard computer opponents, next for normal, and finally for easy\n; right now, normal and hard are the same, because on hard, the computer will actually wait for\n; production on an object to finish if that object is the best target; in this way all three\n; difficulty levels are different.\nAIIonCannonConYardValue=100,100,100\nAIIonCannonWarFactoryValue=50,50,50\nAIIonCannonPowerValue=10,10,40\nAIIonCannonEngineerValue=30,30,5\nAIIonCannonThiefValue=20,20,5\nAIIonCannonHarvesterValue=1,1,1\nAIIonCannonMCVValue=150,150,20\nAIIonCannonAPCValue=15,15,15\nAIIonCannonBaseDefenseValue=35,35,35\nAIIonCannonPlugValue=40,40,40\nAIIonCannonHelipadValue=20,20,20\nAIIonCannonTempleValue=40,40,40\n\n; Ion storm control\nIonLightningFrequency=10  ; Percent chance that lightning will strike this frame\nIonLightningRandomness=90 ; Percent chance that the lightning will strike a random cell instead of an object.\nIonLightningDamage=500    ; Damage done by lightning strike.\nIonStormDuration=120      ; Default ion storm duration in deconds. This is overriden by the trigger control.\nIonStormWarning=31        ; Warning time in seconds before an Ion Storm hits.\nIonStorms=no              ; Are random ion storms going to appear?\nIonStormWarhead=IonWH\t  ; Warhead used by ion storm strike.\n\n; misc\nFogOfWar=no             ; Is fog of war enabled?\nVisceroids=no           ; Are randomly appearing visceroids going to occur?\nMeteorites=no           ; Are tiberium meteorites going to occur?\nCrewEscape=50%          ; percent chance that crew will escape from destroyed vehicle\nCameraRange=9           ; distance around spy camera to reveal map\nFineDiffControl=no      ; Allow 5 difficulty settings instead of only 3 settings?\nPilot=E1                ; pilot type that parachutes out of aircraft\nCrew=E1                 ; soldier that emerges from destroyed unit or building\nTechnician=CTECH        ; civilian infantry type to serve as technician survivor [should be armed variety]\nEngineer=ENGINEER       ; special (limited supply) infantry survivor from construction yards [probably engineer type]\n;EngineerCaptureLevel=0.25\t;commented out because the Engineer's \"enter\" cursor disappears when the value is changed from 1.0\n;EngineerDamage=0.437\t;was 0.0\nDisguise=E1             ; infantry type to appear as when disguised and viewed by the enemy\nParatrooper=E1          ; infantry that is dropped as a paratrooper\n\n; droppod flight characteristics\nDropPodWeapon=DropGun\t; weapon mounted on drop pod\nDropPodHeight=2000      ; height above ground that drop pods appear at\nDropPodSpeed=75         ; speed of drop pod's descent\nDropPodAngle=0.79       ; angle of descent for drop pod [radians; .40=flat,1.18=steep]\n\n; hover vehicle characteristics\nHoverHeight=120         ; height of hovering vehicles\nHoverDampen=40%         ; dampening effect on hover vehicle bounciness\nHoverBob=.04            ; time between hover 'bobs'\nHoverBoost=150%         ; hover speed when traveling on straight away\nHoverAcceleration=.02   ; time to accelerate to full speed\nHoverBrake=.03          ; time to decelerate to full stop\n\n; subterrainean vehicle characteristics\nTunnelSpeed=1\n\n; production & power effects\nMultipleFactory=0       ; factory bonus for multiples [1=full bonus, 0=no bonus] (def=1)\nMinProductionSpeed=.5   ; minimum production speed as result of low power (def=.5)\n\n; hack section\nGDIGateOne=GAGATE_A      ; these buildings affect nearby walls, so I need to know what they are\nGDIGateTwo=GAGATE_B\nWallTower=GACTWR\nNodGateOne=NAGATE_A\nNodGateTwo=NAGATE_B\nNodRegularPower=NAPOWR\nNodAdvancedPower=NAAPWR\nGDIPowerPlant=GAPOWR\nGDIPowerTurbine=GAPOWRUP\nGDIHunterSeeker=GHUNTER\nNodHunterSeeker=NHUNTER\nGDIFirestormGenerator=GAFIRE\n\nRepairBay=GADEPT        ; building to go to when in need of repairs\nPadAircraft=ORCA,ORCAB   ; aircraft that can be produced (and land at) a helipad (or ground)\n\n; AlexB's hack section\nBaseUnit=BASEUNIT\t;whenever this unit appears, it's automatically swapped with the first MCV in the list below that belongs to the player's faction\nHarvesterUnit=HARV,MCV\t;all harvesters that the AI can build, followed by all existing MCVs\n;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\n;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)\n;With exception of units with Harvester=yes, all units in this list are immune to Short Game (like MCVs normally are).\n\n; Bret's hack section\nTreeStrength=200 ; 25\nWindDirection=1         ; Direction of wind (gets converted to a FacingType, so 0 is north\n                        ; and increasing numbers rotate clockwise)\nTrackedUphill=.5        ; coefficient for tracked vehicle movement uphill\nTrackedDownhill=1.1     ; coefficient for tracked vehicle movement downhill\nWheeledUphill=.5        ; coefficient for wheeled vehicle movement uphill\nWheeledDownhill=1.2     ; coefficient for wheeled vehicle movement downhill\nLeptonsPerSightIncrease=2000 ;how high does a unit have to go before it can see farther?\nLeptonsPerFireIncrease=2000 ; how high does a unit have to go before it can fire farther?\nAttackingAircraftSightRange=6\nBlendedFog=yes          ; should we blend the fog (as opposed to dither it)\nCliffBackImpassability=2 ; how impassable is it behind cliffs? (0 = minimal, 2 = maximal)\nIceCrackingWeight=2.0   ; objects weighing more than this will crack ice\nIceBreakingWeight=4.0   ; objects weighing more than this well break through ice\nCloakingStages=9\nTiberiumTransmogrify=40\nTreeFlammability=.05\nCraterLevel=1           ; controls how big the craters from meteorites are.\n                        ; 0 is no cratering, while 4 is the largest craters.\n;StatisticTimeInterval=30; controls how many seconds pass between statistic calculations, for score screen graphs\nBridgeVoxelMax=3        ; maximum debris from each destroyed bridge section (def=3)\nWallBuildSpeedCoefficient=.5  ; how much faster than normal objects do walls build?\nWorstLowPowerBuildRateCoefficient=.3 ; what is the lowest the build rate can get for being low on power?\nBestLowPowerBuildRateCoefficient=.75 ; what is the highest the build rate can get when in a low power condition?\n;ConditionYellowSparkingProbability=0.2\n;ConditionRedSparkingProbability=0.8\nAllowShroudedSubteranneanMoves=true\nAircraftFogReveal=6\nMaximumQueuedObjects=4\nMaxWaypointPathLength=15\n;RevealByHeight=3\n\n; firestorm defense controls\nChargeToDrainRatio=.333\nDamageToFirestormDamageCoefficient=.1\n\n; veinhole monster parameters\n; VeinholeMonsterStrength=1000   ; no longer used.  To modify veinhole monster strength, edit the [VEINTREE] entry\nVeinholeGrowthRate=300\t\t; was 3000\nVeinholeShrinkRate=100\t\t; was 500\nMaxVeinholeGrowth=2000\nVeinDamage=5\nVeinholeTypeClass=VEINTREE\n\n; AI trigger weighting parameters\nAITriggerSuccessWeightDelta=5\nAITriggerFailureWeightDelta=-5\nAITriggerTrackRecordCoefficient=1\n\n; Some spotlight controls\nSpotlightSpeed=.015     ; speed in radians\nSpotlightMovementRadius=2000 ; offset of center of arc sweep\nSpotlightLocationRadius=1000 ; offset from building\nSpotlightAcceleration=.0025    ; acceleration in radians\nSpotlightAngle=.5       ; maximum suggest angle of arc sweep\n\n; Controls for radar events\n; The events, in order, are:\n; (1) Generic Combat Event,\n; (2) Generic Noncombat Event,\n; (3) Dropzone Event,\n; (4) Base Under Attack Event,\n; (5) Harvester Under Attack Event,\n; (6) Enemy Object Sensed Event\n; So, for example, to change the visibility duration of the Harvester Under Attack Event,\n; you would change the fifth number in the list for RadarEventVisibilityDurations\n;\nRadarEventSuppressionDistances=8, 8, 8, 8, 8, 6\t\t\t; suppression distance in cells\nRadarEventVisibilityDurations=200,200,200,200,200,200  ; event visibility in frames\nRadarEventDurations=400,400,400,400,400,400  ;  event duration in frames\nFlashFrameTime=7\nRadarCombatFlashTime=49\t; this should ALWAYS be an odd multiple of FlashFrameTime, ie RadarCombatFlashTime / FlashFrameTime should be an odd number\nRadarEventMinRadius=8\nRadarEventSpeed=1.2\nRadarEventRotationSpeed=.05\nRadarEventColorSpeed=.1\n\nRevealTriggerRadius=9   ; the sight range of a \"reveal around waypoint\" trigger, 10 is maximum\n\n; id holders for particle systems and voxel debris\nExplosiveVoxelDebris=GASTANK,PIECE ; name of explosive voxel debris\nTireVoxelDebris=TIRE    ; name of tire voxel debris\nScrapVoxelDebris=PIECE  ; name of scrap metal voxel debris\nOKBuildingSmokeSystem=SmokeStackSys\nDamagedBuildingSmokeSystem=SmallSmokeSys\nDamagedUnitSmokeSystem=VSSmokeSys\nDebrisSmokeSystem=VSSmokeSys\n\n; Building prerequisite categories are specified here.\nPrerequisitePower=GAPOWR,NAPOWR,NAAPWR\nPrerequisiteFactory=GAWEAP,NAWEAP,DGWEAP,DNWEAP\nPrerequisiteGDIFactory=GAWEAP,DGWEAP\nPrerequisiteNodFactory=NAWEAP,DNWEAP\nPrerequisiteBarracks=NAHAND,GAPILE\nPrerequisiteRadar=GARADR,NARADR\nPrerequisiteTech=GATECH,NATECH\n\n; hunter seeker controls\nHunterSeekerDetonateProximity=150\nHunterSeekerDescendProximity=700\nHunterSeekerAscentSpeed=40\nHunterSeekerDescentSpeed=50\nHunterSeekerEmergeSpeed=6\n\n; default threat evaluation controls\nMyEffectivenessCoefficientDefault=200\nTargetEffectivenessCoefficientDefault=-200\nTargetSpecialThreatCoefficientDefault=200\nTargetStrengthCoefficientDefault=-200\nTargetDistanceCoefficientDefault=-1\t\t; -1 makes AI attack targettype (0,n) scripts to choose nearest enemy instead of first built\n\n; defaults for dumb threat evaluation\nDumbMyEffectivenessCoefficient=200\nDumbTargetEffectivenessCoefficient=200\nDumbTargetSpecialThreatCoefficient=200\nDumbTargetStrengthCoefficient=200\nDumbTargetDistanceCoefficient=-1\n\nEnemyHouseThreatBonus=400\n\n; ******* Jumpjet Flight rules *******\n; Jumpjet movement controls\n[JumpjetControls]\nTurnRate=4\nSpeed=14\nClimb=5\nCruiseHeight=500\t; cruiseheight should be higher than a bridge, just to be safe\nAcceleration=2\nWobblesPerSecond=.15 ; was .25\nWobbleDeviation=40 ; was 40\n\n[LEVITATION]\nDrag=0.1\t\t\t\t\t\t; rate that jellyfish slows down\n\t\t\t\t\t\t\t\t; max velocity that jellyfish can move again when...\nMaxVelocityWhenHappy=5.0\t\t;   ...just puttering around\nMaxVelocityWhenFollowing=4.5\t;\t...going someplace in particular\nMaxVelocityWhenPissedOff=10.0\t;\t...tracking down some mofo\nAccelerationProbability=0.01\t; Chance happy jellyfish will \"puff\"\nAccelerationDuration=20\t\t\t; How long a puff accelerates the jellyfish\nAcceleration=0.75\t\t\t\t; How much a puff accelerates\nInitialBoost=2.0\t\t\t\t; How much of an initial speed boost does jellyfish get when puffing\n;BounceVelocity=3.5\t\t\t\t; How fast does jellyfish bounce away after hitting a wall.  Don't screw with this\n;CollisionWaitDuration=15\t\t; How long does jellyfish wait before puffing after hitting a wall?\nMaxBlockCount=3\t\t\t\t\t; How many times will jellyfish block against a wall before giving up on destination?\nPropulsionSoundEffect=FLOATMOV,FLOTMOV2,FLOTMOV3,FLOTMOV4\t; Sound effect when puffing\nIntentionalDeacceleration=1.0\t; How fast does it deaccelerate when it wants to? (When going to waypoint or target)\nIntentionalDriftVelocity =12.0\t; How fast does it move when it is near its target?\nProximityDistance=3.0\t\t\t; How close before special deacceleration & drift logic take over?\n\n; ******* Special Weapon rules *******\n; Special weapon rules are specified here.\n[SpecialWeapons]\nHSBuilding=GAPLUG,NATMPL ; list of buildings the hunter seeker tries to pop out of\nNukeWarhead=Nuke        ; warhead used by falling nuke missile\nNukeDown=NukeDown       ; nuclear missile as it descends\nNukeProjectile=NukeUp   ; nuclear missile (from silo) projectile to launch\nEMPulseWarhead=EMPuls   ; warhead used by falling nuke missile\nEMPulseProjectile=PulsPr ; nuclear missile (from silo) projectile to launch\n\n; ******* Audio / Visual rules *******\n; General controls that deal with audio or visual appearance of\n; the game or the units therein are specified here.\n[AudioVisual]\nUnloadingHarvester=HORV ; obsolete harvester image to use when unloading tiberium. This now gets overruled by UndeploysInto= on Harvester units.\nPoseDir=2               ; aircraft landing facing (0=N, 1=NE, 2=E, etc)\nDropPodPuff=DROPEXP     ; animation to play when drop pod hits the ground\nWaypointAnimationSpeed=10\t; how fast do the waypoint markers animate?\nBarrelExplode=EXPLOLRG  ; exploding crates animation\nBarrelDebris=GASTANK,PIECE  ; exploding crate debris list\nBarrelParticle=SmallGreySSys\nWake=WAKE2              ; wake effect when traveling on/over water\nVeinAttack=VEINATAC\nDropPod=DROPPOD,DROPPOD2,DROPPODY,DROPPODY2 ; mark to leave after drop pod lands\nDeadBodies=DEATH_A,DEATH_B,DEATH_C,DEATH_D,DEATH_E,DEATH_F   ; choice of dead bodies to leave around\nMetallicDebris=DBRIS1LG,DBRIS2LG,DBRIS3LG,DBRIS4LG,DBRIS5LG,DBRIS6LG,DBRIS7LG,DBRIS8LG,DBRIS9LG,DBRS10LG,DBRIS1SM,DBRIS2SM,DBRIS3SM,DBRIS4SM,DBRIS5SM,DBRIS6SM,DBRIS7SM,DBRIS8SM,DBRIS9SM,DBRS10SM\nBridgeExplosions=TWLT026,TWLT036,TWLT050,TWLT070\t; the explosions to use for the bridge explosion effect\nDigSound=SUBDRIL1       ; sound when digging into the ground\nDig=DIG                 ; anim to play when unit digs into ground\nIonBlast=RING1          ; initial anim when ion cannon hits\nIonBeam=IONBEAM\nInfantryExplode=S_BANG34 ; animation when infantry just explodes\nAtmosphereEntry=PODRING ; animation to use when drop pod enters atmosphere\nGateUp=GATEUP1          ; sound of gate rising\nGateDown=GATEDWN1       ; sound of gate lowering\nShroudGrow=no           ; Does the shroud grow back over time?\nScrollMultiplier=.07     ; multiplier to default scroll speed\nShakeScreen=400         ; divide object strength by this to determine if the screen shakes when destroyed\nCloakSound=CLOAK5       ; sound of cloaking or decloaking\nSellSound=CASHTURN      ; sound of selling objects (typically buildings)\nGameClosed=BLEEP1       ; game closed sound\nIncomingMessage=Message1  ; incoming message sound\nSystemError=BOOP        ; system error sound\nOptionsChanged=Notify   ; options have changed sound\nGameForming=GAMEFRM1    ; game forming sound\nPlayerLeft=BOOP\t\t\t; player has left sound\nPlayerJoined=BOOP     ; player has joined sound\nConstruction=BOOP     ; sound of building construction\nCreditTicks=CREDUP1,CREDDWN1 ; credit tick up and down sounds\nCrumbleSound=CRMBLE2    ; building crumble sound when building is completely destroyed\nBuildingSlam=PLACE2     ; placing building down sound\nRadarOn=COMMUP1         ; radar activation sound\nRadarOff=RADARDN1       ; radar deactivation sound\nScoldSound=SCOLD8       ; generic scold sound\nTeslaCharge=OBELPOWR    ; tesla charge up sound\nTeslaZap=OBELRAY1       ; tesla zap sound\nBlowupSound=EXPNEW01    ; sound when building is damaged to half strength\nChuteSound=BOOP         ; parachute deploy sound\nGenericClick=CLICKY1    ; generic click sound\nGenericBeep=BLEEP1      ; generic beep sound\nBuildingDrop=PLACE2     ; sound of building being placed down\nStopSound=Notify\t\t;Sound when units are commanded to stop\nGuardSound=Notify\t\t;Sound when units are commanded to guard\nScatterSound=Notify\t;Sound when units are commanded to scatter\nDeploySound=27-I002\t;Sound when units are commanded to deploy\nLightningSound=  ; Commented out because sound was way too annoying (AI)\nTreeFire=FIRE2,FIRE1    ; small and large fires to attach to burning trees\nOnFire=FIRE3,FIRE2,FIRE1 ; list of flames to use when something catches fire [must be 3 in list]\nFlamingInfantry=FLAMEGUY  ; anim to use for special onfire infantry logic\nSmoke=xxxx           ; smoke that rises from the ground after a building explosion\nFirestormActiveAnim=GAFSDF_A\nFirestormIdleAnim=FSIDLE\nFirestormGroundAnim=FSGRND\nFirestormAirAnim=FSAIR\nMoveFlash=RING          ; movement destination click feedback animation\nParachute=PARACH        ; big parachute used for paratroopers\nBombParachute=PARABOMB  ; parachute used for parabombs and other parachuted ordinance\nSmallFire=FIRE3         ; animation for small fire [used after napalm]\nLargeFire=FIRE2         ; animation for large fire [used after napalm]\nAllyReveal=yes          ; Allies automatically reveal radar maps to each other?\nConditionRed=25%        ; when damaged to this percentage, health bar turns red\nConditionYellow=50%     ; when damaged to this percentage, health bar turns yellow\nDropZoneRadius=4        ; distance around drop zone flair that map reveals itself\nDropZoneAnim=BEACON     ; animation to use for the drop zone flair\nEnemyHealth=yes         ; Show enemy health bar graph when selected?\nGravity=6               ; gravity constant for ballistic projectiles\nIdleActionFrequency=.15 ; average minutes between infantry performing idle actions\nMessageDelay=.6         ; time duration of multiplayer messages displayed over map\nMovieTime=.06           ; minutes that movie recorder will record when activated (debug version only)\nNamedCivilians=no       ; Show true names over civilians and civilian buildings?\nSavourDelay=.1          ; delay between scenario end and ending movie [keep the delay short]\nShroudRate=4            ; minutes between each shroud creep process [0 means no shadow creep]\nFogRate=.5\nIceGrowthRate=1.5\nIceSolidifyFrameTime=1000 ; how many frames between when ice is cracked and when it gets solidified\nIceCrackSounds=ICECRAK1,ICECRAK2,ICECRAK3\nAmbientChangeRate=.2    ; how many minutes between ambient light recalculations\nAmbientChangeStep=.1    ; step rate for gradually changing ambient lighting\nSpeakDelay=2            ; minutes between EVA repeating advice to the player\nTimerWarning=2          ; if mission timer is less than this many minutes, then display in red\nExtraUnitLight=.2\t; Extra light to make units glow.\nExtraInfantryLight=.2\t; Extra light to make infantry glow.\nExtraAircraftLight=.2\t; Extra light to make aircraft glow.\nEMPulseSparkles=EMP_FX01\t; Anim to play over units disabled by an EM Pulse.\nWebbedInfantry=WEBGUY\n\n; ******* Crate rules *******\n; General crate rules and controls are specified here.\n[CrateRules]\nCrateMaximum=255        ; crates can never exceed this quantity\nCrateMinimum=1          ; crates are normally one per human player but never below this number\nCrateRadius=3.0         ; radius (cells) for area effect crate powerup bonuses\nCrateRegen=3            ; average minutes between random powerup crate regeneration\nSilverCrate=HealBase    ; solo play silver crate bonus\nSoloCrateMoney=2000     ; money to give for money crate in solo play missions\nUnitCrateType=none      ; specifies specific unit type for unit type crate ['none' means pick randomly]\nWoodCrate=Money         ; solo play wood crate bonus\nHealCrateSound=HEALER1    ; heal crate sound effect\nWoodCrateImg=CRATE      ; wood crate overlay image to use\nCrateImg=CRATE          ; normal crate overlay image to use\nFreeMCV=yes             ; Give free MCV from crate if no buildings but still has money [multiplay only]?\n\n; ******* Combat and damage rules *******\n; General rules that control combat, damage, or related items are listed here.\n[CombatDamage]\nAmmoCrateDamage=200     ; damage generated from exploding ammo crate overlay\nIonCannonDamage=751\nHarvesterImmune=no      ; Are harvester immune to normal combat damage?\nDestroyableBridges=yes  ; Can bridges be destroyed?\nTiberiumExplosive=yes   ; Is tiberium extra explosive?\nScorches=BURN01,BURN02,BURN03,BURN04  ; scorch mark smudge types\nScorches1=BURN05,BURN06,BURN07 ; scorch mark smudge types\nScorches2=BURN08,BURN09,BURN10 ; scorch mark smudge types\nScorches3=BURN11,BURN12,BURN13 ; scorch mark smudge types\nScorches4=BURN14,BURN15,BURN16 ; scorch mark smudge types\nTiberiumExplosionDamage = 100 ; the amount of damage dealt out by explosion in a big tiberium chain reaction\nTiberiumStrength = 20 ;\tthe higher this value, the harder it is to get big tiberium to explode\nCraters=CR1,CR2,CR3,CR4,CR5,CR6   ; crater smudge types\nAtomDamage=1000         ; damage points when nuclear bomb explodes (regardless of source)\nBallisticScatter=1.0    ; maximum scatter distance (cells) for inaccurate ballistic projectiles\nBridgeStrength=1500     ; strength of bridge [smaller means more easily destroyed]\nC4Delay=.03             ; minutes to delay after placing C4 before building will explode\nC4Warhead=HE            ; this is the warhead that C4 uses to damage buildings\nFirestormWarhead=FirestormWH ;\tthe warhead that the firestorm defense uses when active\nIonCannonWarhead=IonCannonWH ;\tthe warhead that the ion cannon uses\nVeinholeWarhead=VeinholeWH\n\n;particle system defaults\nDefaultFirestormExplosionSystem=FirestormSparkSys ; the particle system to use when the firestorm defense blows something up\nDefaultLargeGreySmokeSystem=BigGreySmokeSys\nDefaultSmallGreySmokeSystem=SmallGreySSys\nDefaultSparkSystem=SparkSys\nDefaultLargeRedSmokeSystem=BigGreySmokeSys\nDefaultSmallRedSmokeSystem=SmallGreySSys\nDefaultDebrisSmokeSystem=SmallGreySSys\nDefaultFireStreamSystem=FireStreamSys\nDefaultTestParticleSystem=TestSmokeSys\nDefaultRepairParticleSystem=WeldingSys\n\nCrush=1.8               ; if this close (cells) to crushable target, then crush instead of firing upon it (computer only)\nExpSpread=.7            ; cell damage spread per 100 damage points for exploding object types [if Explodes=yes]\nFireSupress=1           ; radius from target to look for friendlies and thus discourage firing upon, if found\nFlameDamage=Fire        ; damage (warhead type) to use when on object is in flames\nFlameDamage2=Fire2\nHomingScatter=2.0       ; maximum scatter distance (cells) for inaccurate homing projectiles\nMaxDamage=1000          ; maximum damage (after adjustments) per shot\nMinDamage=1             ; minimum damage (after adjustments) per shot\nPlayerAutoCrush=no      ; Will player controlled units automatically try to crush enemy infantry?\nPlayerReturnFire=no     ; More aggressive return fire from player controlled objects?\nPlayerScatter=no        ; Will player units scatter, of their own accord, from threats and damage?\n;ProneDamage=50%         ; when infantry is prone, damage is reduced to this percentage\nSplashList=H2O_EXP3,H2O_EXP2,H2O_EXP1 ; water explosion set for conventional explosives\nTreeTargeting=no        ; Automatically show target cursor when over trees?\nTurboBoost=1.5          ; speed multiplier for turbo-boosted weapons when firing upon aircraft\nIncoming=10             ; If an incoming projectile is as slow or slower than this, then\n                        ; object in the target location will try to run away.\n                        ; Grenades have this characteristic.\nCollapseChance=100      ; Percent chance that a cliff will collapse when hit.\nBerzerkAllowed=no       ; Allow Cyborgs to go berzerk when at half damage?\n\n\n; *** Animation List ***\n; This is the complete list of animations available. There are\n; internal tables that rely on this exact order. Additional\n; animations should be appended to the end.\n[Animations]\n1=TWLT100\n3=ELECTRO\n\n; The following can occur in any order.\n240=TWLT026\n241=TWLT036\n242=TWLT050\n243=TWLT070\n244=TWLT100\n245=TWLT070T\n246=TWLT100I\n\n250=S_BANG16\n251=S_BANG24\n252=S_BANG34\n253=S_BANG48\n\n260=S_BRNL20\n261=S_BRNL30\n262=S_BRNL40\n263=S_BRNL58\n\n270=S_CLSN16\n271=S_CLSN22\n272=S_CLSN30\n273=S_CLSN42\n274=S_CLSN58\n\n280=S_TUMU22\n281=S_TUMU30\n282=S_TUMU42\n283=S_TUMU60\n\n290=RING1\n291=IONBEAM\n\n12=SMOKEY\n13=BURN-S\n14=BURN-M\n15=BURN-L\n22=H2O_EXP1\n23=H2O_EXP2\n24=H2O_EXP3\n25=PARACH\n26=PARABOMB\n28=RING\n30=PIFF\n31=PIFFPIFF\n32=FIRE3\n33=FIRE2\n34=FIRE1\n35=FIRE4\n42=GUNFIRE\n43=TWINKLE1\n44=TWINKLE2\n45=TWINKLE3\n47=MONEY\n48=MLTIMISL\n49=HEALONE\n50=HEALALL\n51=ARMOR\n52=CHEMISLE\n53=CLOAK\n54=FIREPOWR\n63=MGUN-N\n64=MGUN-NE\n65=MGUN-E\n66=MGUN-SE\n67=MGUN-S\n68=MGUN-SW\n69=MGUN-W\n70=MGUN-NW\n71=SMOKLAND\n72=VETERAN\n73=REVEAL\n74=SHROUDX\n82=GAPOWR_A\n83=GAPOWR_AD\n84=NARADR_A\n85=NARADR_AD\n90=GAWEAP_1\n91=GAWEAP_2\n92=GAWEAP_A\n93=GAWEAP_B\n94=GAWEAP_C\n95=GAWEAP_D\n96=GAPILE_A\n97=GAPILE_B\n98=NAPULS_A\n99=GACTWR_A\n100=GACTWR_B\n101=GACTWR_C\n102=GACTWR_D\n103=GAPILE_C\n104=GAWEAP_1\n105=GAWEAP_2\n106=GAWEAP_A\n107=GAWEAP_B\n;108=GACOMM_A\n;109=GACOMM_B\n;110=GACOMM_C\n;111=GACOMM_D\n;112=GACOMM_AD\n113=NASTLH_A\n114=NASTLH_AD\n115=GACNSTMK\n116=GACNST_A\n117=GACNST_AD\n118=GACNST_B\n119=GACNST_C\n120=GACNST_CD\n121=GACNST_D\n122=NAHAND_A\n123=NAHAND_B\n124=NAHAND_BD\n125=GAPILE_CD\n126=NATMPL_A\n127=NATMPLMK\n128=NAREFN_A\n129=NAREFN_B\n130=NAREFN_C\n131=GAHPAD_A\n132=GAHPAD_AD\n133=GAPOWR_B\n134=GADEPT_A\n135=GADEPT_AD\n136=GADEPT_B\n137=GATECH_A\n138=GATECH_AD\n139=NATECH_A\n143=NAWAST_A\n144=NAWAST_AD\n145=NAWAST_B\n146=NAWAST_BD\n147=NAOBEL_A\n148=NAMISL_A\n149=NAMISL_AD\n150=NAMISL_B\n151=NAMISL_BD\n152=GAFIRE_A\n153=GAFIRE_B\n154=GAFIRE_C\n155=NAREFN_AR\n156=NAPOST_A\n157=NAPOST_AD\n158=NAPOST_B\n159=WA01X\n160=WA02X\n161=WA03X\n162=WA04X\n163=WB01X\n164=WB02X\n165=WB03X\n166=WB04X\n167=WC01X\n168=WC02X\n169=WC03X\n170=WC04X\n171=WD01X\n172=WD02X\n173=WD03X\n174=WD04X\n175=TREESPRD\n176=NAOBEL_B\n177=GADEPT_C1\n178=GADEPT_C2\n179=GADEPT_C3\n180=GADEPT_D\n181=GADEPT_DD\n182=GASILO_A\n183=GASILO_AD\n184=GASILO_B\n185=GASILO_BD\n186=NAPOWR_A\n187=NAPOWR_AD\n188=CAHOSP_A\n189=NAAPWR_A\n190=NAAPWR_AD\n191=GASPOT_A\n192=GASPOT_AD\n193=CTDAM_A\n194=CTDAM_AD\n195=TUNTOP01\n196=TUNTOP02\n197=TUNTOP03\n198=TUNTOP04\n199=NTPYRA_A\n200=NTPYRA_AD\n201=PULSEFX1\n202=GADPSAMK\n203=METLARGE\n204=METSMALL\n205=METDEBRI\n206=METSTRAL\n207=METLTRAL\n208=PULSBALL\n209=GAFSDF_A\n210=FSIDLE\n211=FSAIR\n212=FSGRND\n213=CAARMR_A\n214=GADPSA_A\n215=GATICK_A\n216=GATICKMK\n;217=UFO\n218=CAARAY_A\n219=CAARAY_B\n220=CAARAY_C\n221=CAARAY_CD\n222=CAARAY_D\n223=CAARAY_DD\n224=GAICBM_A\n225=GAICBMMK\n226=NAHPAD_A\n227=NAHPAD_AD\n228=GAKODK_A\n229=GAKODK_AD\n230=GAKODK_B\n231=GAKODK_C\n232=GAKODK_CD\n233=NAMNTK_A\n234=CTDAM_B\n235=CTDAM_BD\n236=CARYLAND\n237=DROPLAND\n\n300=GAPLUG_A\n301=GAPLUG_B\n302=GAPLUG_BD\n303=GAPLUG_C\n304=GAPLUG_D\n305=GAPLUG_E\n306=GAPLUG_F\n307=GARADR_A\n308=GARADR_AD\n309=NASAM_A\n310=EMP_FX01\n\n\n320=DIG\n\n400=VEINATAC\n\n500=INFDIE\n501=DIRTEXPL\n502=PULSEFX2\n510=DBRIS1LG\n511=DBRIS1SM\n512=DBRIS2LG\n513=DBRIS2SM\n514=DBRIS3LG\n515=DBRIS3SM\n516=DBRIS4LG\n517=DBRIS4SM\n518=DBRIS5LG\n519=DBRIS5SM\n520=DBRIS6LG\n521=DBRIS6SM\n522=DBRIS7LG\n523=DBRIS7SM\n524=DBRIS8LG\n525=DBRIS8SM\n526=DBRIS9LG\n527=DBRIS9SM\n528=DBRS10LG\n529=DBRS10SM\n550=DEATH_A\n551=DEATH_B\n552=DEATH_C\n553=DEATH_D\n554=DEATH_E\n555=DEATH_F\n556=DROPPOD\n557=DROPPOD2\n558=FLAMEGUY\n\n600=EXPLOSML\n601=EXPLOMED\n602=EXPLOLRG\n603=XGRYMED1\n604=XGRYMED2\n605=XGRYSML1\n606=XGRYSML2\n\n610=STEAMPUF\n611=SMOKEY2\n612=PULSE\n613=WAKE1\n614=WAKE2\n618=BEACON\n619=PODRING\n\n620=CLDRNGL1\n621=CLDRNGL2\n622=CLDRNGMD\n623=CLDRNGSM\n\n700=CRYSTAL1\n701=CRYSTAL2\n702=CRYSTAL3\n703=CRYSTAL4\n704=BIGBLUE\n\n705=SGRYSMK1\n706=DROPEXP\n707=INVISO\n\n708=WEBGUY\n709=WEB\n710=K_LIGHT1\n711=K_LIGHT2\n712=MWAR_1\n713=MWAR_2\n714=MWAR_A\n715=MWAR_B\n716=MWAR_C\n717=MWAR_D\n718=MWARMK\n719=DLIMP_A\n720=DJUGG\n721=DJUGG_A\n722=DJUGGMK\n723=MSTLMK\n724=MSTL_A\n725=DEFDMK\n726=CORE_A\n727=CORE_AD\n728=CORE_B\n729=CORE_BD\n730=CORE_C\n731=CORE_CD\n732=OBL1_A\n733=OBL1_AD\n734=OBL1_B\n735=OBL1_BD\n736=OBL1_C\n737=OBL1_CD\n738=OBL2_A\n739=OBL2_AD\n740=OBL2_B\n741=OBL2_BD\n742=OBL2_C\n743=OBL2_CD\n744=DEFD_EXP\n745=MEMPFX\n\n746=NAWEAP_A\n747=NAWEAP_AD\n748=NATECH_AD\n749=BIGBLUE3\n\n\n; ******* Multiplayer Settings *******\n; These are the multiplayer dialog default settings. Does not apply to\n; Westwood chat, only to the in-game dialogs.\n[MultiplayerDefaults]\nMoney=10000\nMaxMoney=10000\nShadowGrow=no\nBases=yes\nTiberiumGrows=yes\nCrates=yes\nCaptureTheFlag=no\n\n; ******* Special weapon charge times *******\n; The time (minutes) for recharge of these special weapons.\n;[Recharge]\n;Nuke=13                 ; nuclear missile\n;EMPulse=5                 ; nuclear missile\n;IonCannon=11\n;FirestormDefense=4\n\n; ******* Object Heap Maximums *******\n; These are the absolute maximum number of these object types\n; allowed in the game (at any one time).\n[Maximums]\nPlayers=8                               ; ipx layer limits this to 8 maximum\n\n\n; ******* AI Controls *******\n; Computer Skirmish-Mode behavior controls. The ratio values are based on the\n; number of buildings in the computer base that should be of the type specified.\n; The ratio total should exceed 100% so that the base will always try to grow as\n; it vainly attempts to achieve the specified percentage composition.\n\n; These AI controls are held over from Red Alert. They will be replaced or augmented\n; by Tiberian Sun improved AI subsystems. Changing these values will be only\n; temporary until the new system comes on line.\n[AI]\nBuildConst=GACNST\nBuildPower=NAPOWR,GAPOWR,NAAPWR  ; buildings to build to generate power\nBuildRefinery=PROC      ; refinery ratio based on these buildings\nBuildBarracks=NAHAND,GAPILE ; barracks ratio based on these buildings\nBuildTech=NATECH,GATECH  ; should build on each of these\nBuildWeapons=GAWEAP,NAWEAP,DGWEAP,DNWEAP       ; war factory ration based on these buildings\nBuildDefense=NAOBEL      ; base defenses are based on these buildings\nBuildPDefense=NAOBEL      ; excess power base defense\nBuildAA=NASAM            ; air defenses based on these buildings\nBuildHelipad=GAHPAD,NAHPAD     ; air helicopter offense based on these buildings\nBuildRadar=GARADR,NARADR\nConcreteWalls=GAWALL,NAWALL\nNSGates=NAGATE_B,GAGATE_B\nEWGates=NAGATE_A,GAGATE_A\nGDIWallDefense=6\nGDIWallDefenseCoefficient=3\nNodBaseDefenseCoefficient=1.2\nGDIBaseDefenseCoefficient=1.5\nMaximumBaseDefenseValue=60\nComputerBaseDefenseResponse=3\t; how much does the computer overrespond to attacks on its base?\n\nAttackInterval=3        ; average delay between computer attacks\nAttackDelay=5           ; average delay time before computer begins first attack\nPatrolScan=.016         ; minute interval between scanning for enemys while patrolling.\nCreditReserve=100       ; Structure repair will not begin if available cash falls below this amount.\nPathDelay=.01           ; Delay (minutes) between retrying when path is blocked.\nBlockagePathDelay=60\t; delay (frames) before unit paths around all blockage\nTiberiumNearScan=6      ; cell radius to scan when harvesting a single patch of Tiberium\nTiberiumFarScan=48      ; cells radius to scan when looking for a new Tiberium patch to harvest\nAutocreateTime=5        ; average minutes between creating an 'autocreate' team\nInfantryReserve=3000    ; always build infantry if cash reserve is greater than this\nInfantryBaseMult=1      ; build infantry if building count times this number is less than current infantry quantity\nPowerSurplus=50         ; build power plants until power surplus is at least this amount\nBaseSizeAdd=3           ; computer base size can be no larger than the largest human opponent, plus this quantity\nRefineryRatio=.16       ; ratio of base that should be composed of refineries\nRefineryLimit=4         ; never build more than this many refineries\nBarracksRatio=.16       ; ratio of base that should be composed of barracks\nBarracksLimit=2         ; never build more than this many barracks\nWarRatio=.1             ; ratio of base that should be composed of war factories\nWarLimit=2              ; never build more than this many war factories\nDefenseRatio=.4         ; ratio of base that should be defensive structures\nDefenseLimit=40         ; maximum number of defensive buildings to build\nAARatio=.14             ; ratio of base that should be anti-aircraft defense\nAALimit=10              ; maximum number of anti-aircraft buildings to build\nTeslaRatio=.16          ; ratio of base that should be telsa coils\nTeslaLimit=10           ; maximum number of tesla coils to build\nHelipadRatio=.12        ; ratio of base that should be composed of helipads\nHelipadLimit=5          ; maximum number of helipads to build\nAirstripRatio=.12       ; ratio of base that should be composed of airstrips\nAirstripLimit=5         ; maximum number of airstrips to build\nCompEasyBonus=no        ; When more than one human in game, computer player goes to \"easy\" mode?\nParanoid=yes            ; Will computer players ally with each other if the situation looks bleak?\nPowerEmergency=75%      ; sell buildings to raise power level if it falls below this percentage\nAIBaseSpacing=1\t\t\t; spacing between buildings when AI is building a base\n\n\n; ******* Lists the AI general COM objects *******\n; These are COM objects that support the IAIHouse interface.\n[AIGenerals]\n;1={F706E6E0-86DA-11D1-B706-00A024DDAFD1}\n;2={9E0F6120-87C1-11D1-B707-00A024DDAFD1}\n;3={C6004D80-87D1-11d1-B707-00A024DDAFD1}\n;4={FBE6D4A0-87D1-11d1-B707-00A024DDAFD1}\n;5={FBE6D4A1-87D1-11d1-B707-00A024DDAFD1}\n\n\n; ******* IQ setting for computer activity *******\n; Each player (computer controlled or otherwise) is given an IQ rating that is used\n; to control what the computer is allowed to automatically control. This is\n; distinct from the difficulty setting. The higher the IQ setting, the more autonomous\n; and intelligent the side will behave. Each ability is given a rating that\n; indicates the IQ level (or above) that the ability will be granted. Because such\n; abilities are automatically performed by the computer, giving a human controlled\n; country a high IQ is not recommended. Otherwise the player's units will start to\n; automatically \"do their own thing\"! A human controlled country is presumed to have\n; an IQ rating of zero. A computer controlled country has an IQ of 1 or higher.\n; When in skirmish mode or when multiplayer AIs are active, the computer IQ is set to\n; the maximum.\n[IQ]\nMaxIQLevels=5           ; the maximum number of discrete IQ levels\nSuperWeapons=4          ; super weapons are automatically fired by computer\nProduction=5            ; building/unit production is automatically controlled by computer\nGuardArea=2             ; newly produced units start in guard area mode\nRepairSell=1            ; allowed to choose repair or sell of damaged buildings\nAutoCrush=2             ; automatically try to crush antogonists if possible\nScatter=2               ; will scatter from incoming threats [grenades and such]\nContentScan=3           ; will consider contents of transport when picking good target\nAircraft=3              ; automatically replace aircraft\nHarvester=2             ; automatically replace lost harvesters\nSellBack=2              ; allowed to sell buildings\n\n\n; ******* Side Type List *******\n; The combantants can be grouped according to side. This\n; lists the sides and their respective member houses.\n[Sides]\nGDI=GDI\nNod=Nod\nCivilian=Neutral\nMutant=Special\n\n; *** House (players) List ***\n; Each side has some basic controls on how they behave (when\n; controlled by the computer. Here is the list of available\n; house types.\n[Houses]\n00=GDI\n01=Nod\n02=Neutral\n03=Special\n\n;This section only affects FinalSun and FinalSun won't be able to start if this section does not exist.\n;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.\n;By default FinalSun only displays the houses of the [Houses] section from FSR.ini and\n;the houses of this [SPHouses] section are only used when you click \"Standard houses\" in FinalSun\n[SPHouses]\n00=GDI\n01=Nod\n02=Neutral\n03=Special\n04=Extra1\n05=Extra2\n06=Extra3\n07=Extra4\n08=Extra5\n09=Extra6\n10=Extra7\n11=Extra8\n\n; ******* Country Statistics *******\n; Certain countries have special adjustments to their unit and building\n; values. These are global values that affect ALL units and buildings owned\n; by that country. This applies only to multiplayer games and skirmish mode. In\n; normal game play, all values are \"1.0\".\n\n; Airspeed = multiplier to speed for all air units [larger means faster] (def=1.0)\n; Armor = multiplier to armor strength for all units and buildings [larger means stronger] (def=1.0)\n; Cost = multiplier to cost for all units and buildings [larger means costlier] (def=1.0)\n; Firepower = multiplier to firepower for all weapons [larger means more damage] (def=1.0)\n; Groundspeed = multiplier to speed for all ground units [larger means faster] (def=1.0)\n; ROF = multiplier to Rate Of Fire for all weapons [larger means slower ROF] (def=1.0)\n; BuildTime = multiplier to general object build time [larger means longer to build] (def=1.0)\n; Color = color to use when displaying objects owned by this country [see color schemes]\n; Multiplay = This house used as placeholder for multiplay house (def=no)?\n; WallOwner = Will this house own walls that are placed near its buildings (def=yes)?\n; SmartAI = Does it presume to have the smart AI logic already enabled (def=no)?\n\n[GDI]\nName=GDI\nSuffix=GDI\nPrefix=G\nColor=Gold\nMultiplay=yes\nSide=GDI\n\n[Nod]\nName=NOD\nSuffix=NOD\nPrefix=N\nColor=DarkRed\nMultiplay=yes\nSide=Nod\nSmartAI=yes\n\n[Special]\nName=JP\nSuffix=JP\nPrefix=J\nColor=Grey\nSide=Mutant\nSmartAI=yes\nMultiplayPassive=true\n\n[Neutral]\nName=Civilian\nSuffix=CIV\nPrefix=C\nColor=Grey\nMultiplayPassive=true\nSmartAI=yes\nSide=Civilian\n\n\n\n; ******* Color Schemes *******\n; Each country must be assigned a color. This lists the various\n; colors available. The values represent the 'hue', 'saturation',\n; and 'value'. The 'value' component specifies the maximum brightness\n; allowed for the color as the color spread is generated. The 'hue'\n; component remains constant. The 'saturation' curves through color\n; space as the 'value' component changes such that darker colors\n; become more saturated.\n[Colors]\n; Col. Mustard\nLightGold=34,128,255\t\t; 0 - TopBar - Options, Credit\nGold=34,160,255\t\t\t; 1\nDarkGold=34,235,255\t\t; 2\n\n; Mrs White\nLightGrey=0,0,220\t\t; 3 - Civilians, CameoText, QueueCount\nGrey=0,0,190\t\t\t; 4\nDarkGrey=0,0,120\t\t; 5\nBlack=0,100,0\t\t\t; 6\n;as white as we can get\nWhite=0,0,255\t\t\t; 7\n\n; Miss Scarlet\nLightRed=0,70,255\t\t; 8\nRed=0,160,255\t\t\t; 9\nDarkRed=0,235,255\t\t; 10\nBurgandy=0,255,150\t\t; 11\n\n;Orange Julius\nLightOrange=24,165,255\t\t; 12\nOrange=24,255,255\t\t; 13\nDarkOrange=11,235,255\t\t; 14\n\n; Mrs Peacock\nLightMagenta=228,120,255\t; 15\nMagenta=228,160,255\t\t; 16\nDarkMagenta=228,235,255\t\t; 17\n\n; Prof. Plum\nLightPurple=200,160,255\t\t; 18\nPurple=200,235,255\t\t; 19\nHyundaiPurple=200,235,170\t; 20\n\n;Little Boy Blue\nLightBlue=164,140,255\t\t; 21 - CameoText Ready for Buildings/Superweapon\nBlue=164,200,255\t\t; 22\nDarkBlue=164,200,179\t\t; 23\nNeonBlue=164,255,255\t\t; 24\n\n;Sky\nLightSky=142,70,255\t\t; 25\nSky=142,160,255\t\t\t; 26\nDarkSky=142,235,255\t\t; 27\n\n;Cyan\nLightCyan=132,70,255\t\t; 28\nCyan=132,160,255\t\t; 29\nDarkCyan=132,235,255\t\t; 30\n\n;Teal\nLightTeal=110,70,255\t\t; 31\nTeal=110,160,255\t\t; 32\nDarkTeal=110,235,255\t\t; 33\n\n; Mr. Green\nLightGreen=85,70,255\t\t; 34\nGreen=85,160,200\t\t; 35\nDarkGreen=85,235,150\t\t; 36\nNeonGreen=85,255,255\t\t; 37\n\n;Mellow Yellow\nLightYellow=43,70,255\t\t; 38\nYellow=43,160,255\t\t; 39\nDarkYellow=43,235,255\t\t; 40\nNeonYellow=43,255,255\t\t; 41\n\n;Life is Peachy\nLightPeach=21,120,255\t\t; 42\nPeach=21,150,255\t\t; 43\nDarkPeach=21,180,255\t\t; 44\nDarkerPeach=21,255,255\t\t; 45\n\n;Lemon lime\nLightLime=53,70,255\t\t; 46\nLime=53,160,255\t\t\t; 47\nDarklime=53,235,200\t\t; 48\nNeonLime=53,235,255\t\t; 49\n\n\n; ******* Difficulty Settings *******\n; Game difficulty is controlled by these factors. Some of these factors will\n; only affect a computer player. The computer and the player are handicapped by\n; individual settings. Thus the computer may be playing at 'difficult' level while the\n; player may be playing at 'easy' level.\n\n; Airspeed = multiplier to speed for all air units (def=1.0)\n; Armor = multiplier to armor strength for all units and buildings (def=1.0)\n; Cost = multiplier to cost for all units and buildings (def=1.0)\n; Firepower = multiplier to firepower for all weapons (def=1.0)\n; Groundspeed = multiplier to speed for all ground units (def=1.0)\n; ROF = multiplier to Rate Of Fire for all weapons [larger means slower ROF] (def=1.0)\n; BuildSlowdown = Should the computer build slower than the player (def=no)?\n;  <<< affects the computer player, not the human player >>>\n;    ContentScan = Should the contents of a transport be considered when picking best target (def=no)?\n;    RepairDelay = average delay (minutes) between initiating building repair\n;    BuildDelay = average delay (minutes) between initiating construction\n;    DestroyWalls = Allow scanning for nearby enemy walls and destroy them (def=yes)?\n\n[Easy]\nGroundspeed=1.0\nAirspeed=1.0\nBuildTime=.8\nArmor=1.2\nROF=.8\nCost=1.0\nRepairDelay=.02\nBuildDelay=.03\nDestroyWalls=yes\nContentScan=yes\n\n[Normal]\nGroundspeed=1.0\nAirspeed=1.0\nBuildTime=1\nArmor=1.0\nROF=1.0\nCost=1.0\nRepairDelay=.02\nBuildDelay=.03\nBuildSlowdown=yes\nDestroyWalls=yes\nContentScan=yes\n\n[Difficult]\nGroundspeed=1.0\nAirspeed=1.0\nBuildTime=1.0\nArmor=.8\nROF=1.2\nCost=1.0\nRepairDelay=.05\nBuildDelay=.1\nBuildSlowdown=yes\nDestroyWalls=no\n\n\n; ******* Unit Statistics *******\n; Specifies the characteristics of the various game objects.\n\n; AllowedToStartInMultiplayer = Can the unit be allocated to a player when starting a multiplayer game (def=yes)\n; Ammo = number of rounds carried between reloads [-1 means unlimited] (def=-1)\n; Armor = the armor type of this object [none,wood,light,heavy,concrete] (def=none)\n; BuildLimit = arbitrary maximum allowed to build [per house] (def=-1 -- no restriction)\n; Cloakable = Is it equipped with a cloaking device (def=no)?\n; Cost = cost to build object (in credits)\n; Category = category of object [used by AI systems -- \"Soldier\", \"Civilian\", \"VIP\", \"Ship\",\n;            \"Recon\", \"AFV\", \"IFV\", \"LRFS\", \"Support\", \"Transport\", \"AirPower\", \"AirLift\"]\n; CloakStop = Does the unit cloak when stopped moving (def=no)?\n; Crewed = Does it contain a crew that can escape [never infantry] (def=no)?\n; CrushSound = sound to play if this object type is crushed (def=none)\n; DeployTime = time, in minutes, to deploy or undeploy [if this object can do so]\n; Disableable = Can this object be disabled by special multiplay option (def=yes)?\n; DoubleOwned = Can be built/owned by all countries in a multiplayer game (def=no)?\n; Explodes = Does it explode violently when destroyed [i.e., does it do collateral damage] (def=no)?\n; Explosion = the explosion to use when it blows up [doesn't apply to infantry] (def=none)\n; FireAngle = pitch of projectile launch [64 = horizontal, 0 = vertical] (def=50)\n; Gate = Is this building a gate? (def=no)\n; GateCloseDelay = time, in minutes, to delay before closing a gate after it has opened.\n; GuardRange = distance to scan for enemies to attack (def=use weapon range)\n; Image = name of graphic data to use for this object (def=same as object identifier)\n; Immune = Is this object immune to damage\n; ImmuneToVeins = Is it immune to vein creature attacks (def=no)?\n; Invisible = Is completely and always invisible to enemy (def=no)?\n; Insignificant = Will this object not be announed when destroyed (def=no)?\n; LegalTarget = Is this allowed to be a combat target (def=yes)?\n; Name = specifies the given name (displayed) for the object\n; Nominal = Always use the given name rather than generic \"enemy object\" (def=no)?\n; Owner = who can build this [GDI or Nod] (def=none)\n; PipScale = what to base pip display on [Passengers, Tiberium, Ammo, Power] (def=none)\n; Points = point value for scoring purposes (def=0)\n; Prerequisite = list of buildings needed before this can be manufactured (def=no requirement)\n; Primary = primary weapon equipped with (def=none)\n; Secondary = secondary weapon equipped with (def=none)\n; Elite = new primary weapon when at elite veteran status (def=same as primary)\n; RadarVisible = Is visible on radar even when under shroud (def=yes [infantry=no])?\n; ROT = Rate Of Turn for body (if present) and turret (if present) (def=0)\n; Reload = time delay between reloads (def=0)\n; RadarInvisible = Is it invisible on radar maps (def=no)?\n; SelfHealing = Does the object heal automatically up to half strength (def=no)?\n; Selectable = Can this object be selected by the player (def=yes)?\n; Sensors = Has sensors to detect nearby cloaked objects (def=no)?\n; Sight = sight range, in cells (def=1)\n; Storage = the number of 'bails' this building or unit can store (def=0)\n; Strength = strength (hit points) of this object\n; TargetLaser = Does it have a targeting laser (def=no)?\n; Trainable = Can this object become veteran by experience (def=yes, buildings def=no)?\n; Turret = Is it equipped with a turret like superstructure [never infantry] (def=no)?\n; TurretSpins = Does the turret just sit and spin [only if turret equipped] (def=no)?\n; TechLevel = tech level required to build this [-1 means can't build] (def=-1)\n; ToProtect = Should friendly units come to rescue if under attack [computer only] (def=no)?\n; TypeImmune = Immune to damage from same type objects if owned by same side?\n; VoiceSelect = list of voices when selecting this object (def=none)\n; VoiceMove = list of voices to use when giving object a movement order (def=none)\n; VoiceAttack = list of voices to use when giving object an attack order (def=none)\n; VoiceDie = list of voices to use when it dies (def=none)\n; VoiceFeedback = list of voices that may give when taking damage (def=none)\n; Locomotor = CLSID of the object handling movement for this object (def=statue)\n; VeteranAbilities = list of veteran abilities to grant (def=none)\n; EliteAbilities = list of elite abilities to grant cumulative with veteran abilities (def=none)\n;     [FASTER,STRONGER,FIREPOWER,SCATTER,ROF,SIGHT,\n;      CLOAK,TIBERIUM_PROOF,VEIN_PROOF,SELF_HEAL,EXPLODES,\n;      RADAR_INVISIBLE,SENSORS,FEARLESS,C4,TIBERIUM_HEAL,\n;      GUARD_AREA,CRUSHER]\n; NonVehicle = Are repair units unable to repair this unit?\n; TooBigForCarryalls = Should Carryalls be unable to airlift this (def=no)?\n; IsVehicleTransport = Is this able to carry vehicles as passengers (def=no)?\n;  <<< applies only to infantry types >>>\n;    Agent = Does it have spy-like abilities (def=no)?\n;    Fearless = Is not prone to fear (def=no)?\n;    VoiceComment = list of idle voices (def=none)\n;    Pip = color of pip when inside a transport [green,yellow,white,red,blue] (def=green)\n;    C4 = Equipped with building sabotage explosives [presumes Infiltrate is true] (def=no)?\n;    Cyborg = Does it require special cyborg death handling (def=no)?\n;    Fraidycat = Is it inherently afraid and will panic easily (def=no)?\n;    TiberiumProof = Is it immune to tiberium and tiberium gas damage (def=no)?\n;    Infiltrate = Can it enter a building like a spy or thief (def=no)?\n;    IsCanine = Should special case dog logic be applied to this?\n;    Civilian = Counts a civilian for evac and kill tracking (def=no)?\n;    FemaleVoice = Uses the civilian female voice (def=no)?\n;    Engineer = Does it behave like an engineer as far as repair and capture go (def=no)?\n;    Disguised = Is it disguised as enemy soldier when seen by enemy (def=no)?\n;    Agent = Does this infantry gather information if it enters an enemy building [like a spy] (def=no)?\n;    Mechanic = Can this infantry repair vehicles (def=no)?\n;    VehicleThief = Does it steal enemy vehicles when it gets close to one (def=no)?\n;  <<< applies only to moving units (not buildings) >>>\n;    MoveToShroud = Allowed to move into a shrouded cell (def=yes, aircraft def=no)?\n;    Dock = preferred docking building [e.g., harvester -> refinery, helicopter -> helipad] (def=none)\n;    TiberiumHeal = Does it heal slowly when in Tiberium field (def=no)?\n;    Passengers = number of passengers it may carry (def=0)\n;    Speed = speed of this object [n/a for buildings] (def=0)\n;    ManualReload = Must this object reload by coordinating with reloader building (def=no)?\n;    WalkRate = walking animation rate [larger means slower] (def=1)\n;  <<< applies only to terrestrial driving vehicle types >>>\n;    CrateGoodie = Can it appear out of a crate in multiplay (def=no)?\n;    Crushable = Can it be crushed by a heavy tracked vehicle (def=no)?\n;    Crusher = Is this vehicle able to crush infantry (def=no)?\n;    NoMovingFire = The vehicle must stop before it can fire (def=no)?\n;    DeployToFire = The vehicle must deploy before it can fire (def=no)?\n;    Harvester = Does the special Tiberium harvesting rules apply (def=no)?\n;    Weeder = Does the special weed-harvesting rules apply (def=no)?\n;    Deployer = Does it deploy before being able to operate (def=no)? OBSOLETE\n;    IsTilter = Does this unit tilt on slopes (def=yes)?\n;    CarriesCrate = Might this unit drop a crate when it is destroyed (def=no)?\n;  <<< applies only to aircraft >>>\n;    Carryall = Can it tote vehicles around (def=no)?\n;    Landable = Can this aircraft land on the map (def=no)?\n;    PitchSpeed = Throttle setting at which aircraft pitch forward (def=.25);\n;    PitchAngle = Amount that non-FixedWing aircraft pitch forward in degrees (def=20.0);\n;    RollAngle = Amount that the aircraft rolls when turning (def=30.0)\n;  <<< applies only to building types >>>\n;    Adjacent = distance allowed to place from other buildings (def=1)\n;    BaseNormal = Considered for building adjacency checks (def=yes)?\n;    Barrel = Use barrel explosion logic when it is destroyed (def=no)?\n;    Bib = Does the building have a bib built in (def=no)?\n;    Capturable = Can this building be infiltrated by a spy/engineer (def=no)?\n;    DockUnload = When a unit docks with this building should it unload (def=no)?\n;    Factory = type of object to build [InfantryType, AircraftType, UnitType, BuildingType, VesselType] (def=none)\n;    Fake = Is this a fake structure (def=no)?\n;    FreeUnit = free unit to give this building [typically harvester with refinery] (def=none)\n;    Power = power output [positive for output, negative for drain] (def=0)\n;    Powered = Does it require power to function (def=no)?\n;    Radar = Does this building give radar to owning player (def=no)?\n;    Repairable = Can it be repaired (def=yes)?\n;    UnitReload = Does this building reload units if they dock with it (def=no)?\n;    UnitRepair = Does this building repair units if they dock with it (def=no)?\n;    Unsellable = Cannot sell this building (even if it can be built)?\n;    Wall = Is this a wall type structure [special rules apply] (def=no)?\n;    WaterBound = Is this building placed on water only (def=no)?\n;    Upgrades = Is the number of power-ups/upgrades that can be applied to this building (def=0)\n;    ShipYard = This building is a ship yard or sub pen\n;    SAM = This building is a SAM launcher\n;    ConstructionYard = This building is a construction yard\n;    Refinery = This building is a tiberium/ore refinery\n;    WeaponsFactory = This building is a weapons factory\n;    CloakGenerator = Does this building cloak objects around it?\n;    LaserFencePost = This building is a laser fence post and obeys the rules for a building of this type.\n;    LightIntensity = This building radiates this amount of light (def = 0).\n;    LightVisibility= The distance (in leptons) that this light is visible from (def=5000).\n;    LightRedTint   = The red tint of this buildings light (def=1.0)\n;    LightGreenTint = The green tint of this buildings light (def=1.0)\n;    LightBlueTint  = The blue tint of this buildings light (def=1.0)\n;    InvisibleInGame= Building cannot be seen on selected in the game, only in the editor. (def=no)\n;    PowersUpBuilding = Building that can be upgraded by attaching this building to it\n;    PowersUpToLevel = Amount of upgrade provided by this attachment. -1=incremental upgrade. Positive number is specific upgrade.\n;    Hospital = Can this building heal infantry (def = no) ?\n;    Armory = Is this building an armory\n;    PlaceAnywhere = Can this building ignore normal placement rules? Only use this for non-player placed buildings (def = no).\n;    Weeder = Is this a weed collection facility (def=no)?\n;    TogglePower = [override] Can be turned on/off under player control or affected by low power (def=yes)?\n;    TurretChargeAnimRate = The rate at which this building charges before firing a weapon (higher is slower, def = 3).\n;    ProduceCashAmount = Gives the specified amount of credits to the owner of the structure every 180 frames (def = 0, max = 255).\n;\n;\t WST 6/23/99. Below are new zbuffer adjustment for units\n;\t ZFudgeCliff // fudge for units behind cliffs showing through rocks\n;\t ZFudgeColumn // fudge for units behind bridge overpass support columns\n;\t ZFudgeTunnel // fudge for unit behind tunnel entrances\n;\t ZFudgeBridge // fudge for tall units when they are under a bridge... eg mammoth mk2\n;\n\n\n\n; ******* Vehicle Types *******\n; This lists all of the vehicles types in the game. Each vehicle\n; type should have a matching section that specifies the data it\n; requires.\n[VehicleTypes]\n00=HARV\t;index 0 is used in fsgdi07.map\n01=HORV\n02=APC\n03=BIKE\n04=MMCH\n05=TTNK\n06=REPAIR\n07=SMECH\n08=BGGY\n09=FLMTNK\n10=HVR\n11=ART2\n12=LPST\n13=JUGG\n14=LIMPET\n15=MOBILEMP\n16=CMOBILEMP\n17=MCV\n18=SAPC\n19=STNK\n20=SUBTANK\n21=SONIC\n22=REAPER\n23=4TNK\n24=HMEC\n25=MOBWARG\n26=MOBWARN\n27=SGEN\n28=WEED\n29=BASEUNIT\n30=GHUNTER\n31=NHUNTER\n32=DEFENDER\n33=CAR\n34=PICK\n35=WINI\n36=BUS\n37=TRUCKA\n38=TRUCKB\n39=ICBM\n40=LOCOMOTIVE\n41=TRAINCAR\n42=CARGOCAR\n43=VISC_SML\n44=VISC_LRG\n45=JFISH\n\n; Hover MLRS (hover multi-launch rocket system)\n[HVR]\nName=Hover MLRS\nCategory=AFV\nTargetLaser=yes\nFireAngle=32\nPrerequisite=GDIFACTORY,GARADR\nPrimary=HoverMissile\nTooBigToFitUnderBridge=true\nStrength=230\nArmor=wood\nTechLevel=7\nCrateGoodie=yes\nSight=7\nSpeed=7\nOwner=GDI\nCost=900\nTurret=yes\nPoints=30\nROT=5\nCrusher=no\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nSpeedType=Hover\nLocomotor={4A582742-9839-11d1-B709-00A024DDAFD1}\nMovementZone=AmphibiousDestroyer\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nEliteAbilities=SELF_HEAL\nZFudgeColumn=12\nZFudgeTunnel=15\n\n; Mammoth tank\n[4TNK]\nName=Mammoth Tank\nCategory=AFV\nTargetLaser=yes\nPrimary=120mmx\nSecondary=MammothTusk\nStrength=600\nCrateGoodie=yes\nArmor=heavy\nTurret=yes\nTechLevel=-1\nSight=6\nSpeed=4\nOwner=GDI\nCost=1700\nPoints=60\nROT=5\nCrusher=yes\nSelfHealing=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Destroyer\nThreatPosed=40\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nAllowedToStartInMultiplayer=no\nZFudgeColumn=9\nZFudgeTunnel=15\n\n[TRUCKA]\nName=Truck\nCategory=AFV\nPrimary=none\nSecondary=none\nStrength=200\nArmor=light\nTurret=no\nTechLevel=-1\nSight=5\nSpeed=4\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=500\nPoints=40\nROT=5\nCrusher=no\nSelfHealing=no\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=2\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nMaxDebris=2\nDebrisTypes=TIRE\nDebrisMaximums=4\n\n[TRUCKB]\nName=Truck (loaded)\nCategory=AFV\nPrimary=none\nSecondary=none\nStrength=200\nArmor=light\nTurret=no\nTechLevel=-1\nSight=5\nSpeed=4\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=500\nPoints=40\nROT=5\nCrusher=no\nSelfHealing=no\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=2\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=2\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nDebrisTypes=TIRE\nDebrisMaximums=4\nCarriesCrate=yes\n\n; Deployable Sensor Array\n[LPST]\nName=Mobile Sensor Array\nCategory=Support\nPrerequisite=FACTORY,RADAR\nStrength=600\n;CloakRadiusInCells=20\nRadarInvisible=yes\nArmor=wood\nTechLevel=6\nSight=10\nSpeed=6\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nTurret=no\nCost=950\nPoints=30\nROT=5\nDeploysInto=GADPSA\nCrusher=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=3\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nZFudgeColumn=8\nZFudgeTunnel=15\n\n; ICBM launcher\n[ICBM]\nName=Missile Launcher\nCategory=Support\nPrerequisite=NAWEAP,NARADR\nStrength=500\nArmor=light\nTechLevel=-1\nSight=7\nSpeed=6\nOwner=Nod\nAllowedToStartInMultiplayer=no\nTurret=no\nCost=1400\nPoints=30\nROT=5\nDeploysInto=GAICBM\nCrusher=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nDebrisTypes=TIRE\nDebrisMaximums=4\nZFudgeColumn=18\nZFudgeTunnel=18\n\n; repair vehicle\n[REPAIR]\nName=Mobile Repair Vehicle\nCategory=Support\nPrerequisite=NODFACTORY\nPrimary=RepairBullet\nStrength=200\nArmor=light\nTechLevel=7\nSight=5\nSpeed=6 ; Dropped from 8\nOwner=Nod\nAllowedToStartInMultiplayer=no\nTurret=no\nCost=1000\nPoints=30\nROT=5\nCrusher=yes\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=3\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SmallGreySSys ; The repair bot should not have a spark particle system in here!\nGuardRange=8\nSpecialThreatValue=1\nZFudgeColumn=10\nZFudgeTunnel=14\n\n; advanced mobile artillery\n; This unit needs a primary weapon type to allow targetting but it isn't actually\n; allowed to fire unless deployed.\n[ART2]\nName=Artillery\nFireAngle=42\nPrerequisite=NODFACTORY,NARADR\nPrimary=155mm\nCategory=LRFS\nStrength=300\n;DeployTime=1.0\nTurret=no\nDeploysInto=GAARTY\nArmor=light\nTechLevel=6\nSight=9\nSpeed=5\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=975\nPoints=35\nCrateGoodie=yes\nROT=2\nCrusher=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nNoMovingFire=yes\nDeployToFire=yes\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Crusher\nThreatPosed=10\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=SELF_HEAL\nZFudgeColumn=10\nZFudgeTunnel=14\n\n; Weed-eater vehicle\n[WEED]\nName=Weed Eater\nPrerequisite=NODFACTORY,NAWAST\nToProtect=yes\nCategory=Support\nStrength=600\nArmor=heavy\nDock=NAWAST\nTechLevel=10\nSight=4\nWeeder=yes\nSpeed=5\nOwner=Nod\nAllowedToStartInMultiplayer=no\nPipScale=Tiberium\nStorage=7\nCost=1400\nPoints=55\nROT=5\nCrusher=yes\nCrewed=yes\nSelfHealing=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nThreatAvoidanceCoefficient=.6\nDamageParticleSystems=SparkSys,SmallGreySSys\nImmuneToVeins=yes\nZFudgeColumn=7\nZFudgeTunnel=12\n\n; harvester\n[HARV]\nName=Harvester\nPrerequisite=FACTORY,PROC\nNominal=yes\nToProtect=yes\nCategory=Support\nExplodes=yes\nStrength=1000\nArmor=heavy\nDock=PROC\nHarvester=yes\nUndeploysInto=HORV\t;this specifies the \"harvester without back\" unit\nTechLevel=1\nSight=4\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nPipScale=Tiberium\nCrateGoodie=yes\nStorage=28\nCost=1400\nPoints=55\nROT=5\nCrusher=yes\nAutoCrush=yes\nCrewed=yes\nSelfHealing=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=1\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nThreatAvoidanceCoefficient=.65\nDamageParticleSystems=SparkSys,SmallGreySSys\nImmuneToVeins=yes\nZFudgeColumn=9\nZFudgeTunnel=14\nZFudgeBridge=7\n\n; harvester without back\n[HORV]\nName=Harvester\nNominal=yes\nToProtect=yes\nCategory=Support\nStrength=1000\nArmor=heavy\nDock=PROC\nHarvester=yes\nTechLevel=-1\nSight=4\nSpeed=8\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=1400\nPoints=25\nROT=5\nCrusher=yes\nCrewed=yes\nSelfHealing=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=1\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nThreatAvoidanceCoefficient=1\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Mobile Construction Vehicle\n[MCV]\nName=Mobile Construction Vehicle\nPrerequisite=FACTORY,TECH\nStrength=1000\nCategory=Support\nArmor=heavy\nDeploysInto=GACNST\nTechLevel=10\nSight=6\nSpeed=3\nOwner=GDI,Nod\nCrateGoodie=no\t;[BASEUNIT] already handles this\nCost=2500\nPoints=60\nROT=5\nCrewed=yes\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=12\nZFudgeTunnel=15\nAllowedToStartInMultiplayer=no\n\n;BASEUNIT is a dummy unit that can never actually appear ingame,\n;but instead it's always swapped with the MCV of the receiving player's faction.\n;All MCVs that BASEUNIT can be swapped out with are to be specified after HarvesterUnit=, following the harvesters.\n[BASEUNIT]\nImage=MCV\nName=Mobile Construction Vehicle\nCrateGoodie=yes\n\n; Amphibious APC\n[APC]\nName=Amphibious APC\nPrerequisite=GDIFACTORY,GAPILE\nStrength=200\nCategory=Transport\nArmor=heavy\nDeployTime=.022\nTechLevel=6\nSight=5\nPipScale=Passengers\nSpeed=8\nCrateGoodie=yes\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nCrusher=yes\nPassengers=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=6\nSpeedType=Amphibious\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=AmphibiousCrusher\nThreatPosed=10\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=10\nZFudgeTunnel=13\n\n; School Bus\n[BUS]\nName=School Bus\nStrength=100\nNominal=yes\nCategory=Transport\nDeployTime=.022\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nCrusher=yes\nPoints=25\nROT=5\nPassengers=20\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.9\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Train Locomotive\n[LOCOMOTIVE]\nName=Locomotive\nNominal=yes\nImage=MONOENG\nStrength=100\nCategory=Transport\nDeployTime=.022\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nCrusher=yes\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=2\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=5\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.9\nMovementRestrictedTo=Railroad\nSlowdownDistance=700\nDeaccelerationFactor=0.001\nAccelerationFactor=0.01\nIsTrain=yes\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Train car\n[TRAINCAR]\nName=Train Car\nNominal=yes\nImage=MONOCAR\nStrength=100\nCategory=Transport\nDeployTime=.022\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nCrusher=yes\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=10\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.9\nMovementRestrictedTo=Railroad\nPassive=yes\nIsTrain=yes\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Cargo car for train\n[CARGOCAR]\nName=Cargo Car\nNominal=yes\nImage=CARGOCAR\nStrength=100\nCrusher=yes\nCategory=Transport\nDeployTime=.022\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=10\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.9\nMovementRestrictedTo=Railroad\nPassive=yes\nIsTrain=yes\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nCarriesCrate=yes\n\n; pickup truck\n[PICK]\nName=Pickup Truck\nStrength=100\nNominal=yes\nCategory=Transport\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=2\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Civilian automobile\n[CAR]\nName=Automobile\nStrength=100\nCategory=Transport\nNominal=yes\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=4\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n; Small Visceroid\n[VISC_SML]\nName=Baby Visceroid\nNominal=yes\nInsignificant=yes\nImage=VISSML\nStrength=200\nCategory=Civilian\nArmor=light\nTechLevel=-1\nSight=0\nSpeed=8\nOwner=Civilian\nAllowedToStartInMultiplayer=no\nCost=1\nPoints=50\nROT=16\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nTiberiumHeal=yes\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Normal\nSmallVisceroid=yes\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nTrainable=no\nNonVehicle=yes\n\n; Large Visceroid\n[VISC_LRG]\nName=Adult Visceroid\nInsignificant=yes\nImage=VISLRG\nNominal=yes\nAltImage=VISLGATK\nStrength=500\nCategory=Civilian\nArmor=heavy\nTechLevel=-1\nSight=0\nSpeed=8\nTiberiumHeal=yes\nOwner=Civilian\nAllowedToStartInMultiplayer=no\nCost=1\nPoints=50\nROT=16\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Normal\nLargeVisceroid=yes\nThreatPosed=20\t; This value MUST be 0 for all building addons\nPrimary=SlimeAttack\nGuardRange=5\nImmuneToVeins=yes\nTrainable=no\nNonVehicle=yes\n\n; Hunter-Seeker Droid\n[GHUNTER]\nName=GDI Hunter-Seeker\nImage=GGHUNT\nStrength=500\nInsignificant=yes\nCategory=AFV\nPrimary=SuicideBomb\nPrerequisite=GAPLUG2\nArmor=light\nTechLevel=-1\nSight=7\nSpeed=25\nFlightLevel=400\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=1000\nPoints=50\nROT=16\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nThreatPosed=10\t; This value MUST be 0 for all building addons\nGuardRange=5\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nHunterSeeker=yes\nAlternateSpeed=10\t\t\t; this value is just used when exiting the war factory\nAlternateFlightLevel=50\t\t; this value is just used when exiting the war factory\nSelectable=false\nIgnoresFirestorm=yes\n\n; Hunter-Seeker Droid\n[NHUNTER]\nName=Nod Hunter-Seeker\nImage=GGHUNT\nStrength=500\nInsignificant=yes\nCategory=AFV\nPrimary=SuicideBomb\nPrerequisite=NATMPL\nArmor=light\nTechLevel=-1\nSight=7\nSpeed=25\nFlightLevel=400\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=1000\nPoints=50\nROT=16\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nThreatPosed=10\t; This value MUST be 0 for all building addons\nGuardRange=5\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nHunterSeeker=yes\nAlternateSpeed=10\t\t\t; this value is just used when exiting the war factory\nAlternateFlightLevel=50\t\t; this value is just used when exiting the war factory\nSelectable=false\nIgnoresFirestorm=yes\n\n; Recreational Vehicle\n[WINI]\nName=Recreational Vehicle\nNominal=yes\nStrength=200\nCategory=Transport\nArmor=light\nTechLevel=-1\nSight=5\nPipScale=Passengers\nSpeed=8\nCrateGoodie=yes\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nPassengers=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=4.0\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nImmuneToVeins=yes\n\n; Medium Mech\n[MMCH]\nName=Titan\nWalkRate=2\nImage=MMCH\nPrerequisite=GDIFACTORY\nPrimary=120mm\nStrength=400\nCategory=AFV\nArmor=heavy\nTurret=yes\nIsTilter=no\nTargetLaser=yes\nTooBigToFitUnderBridge=true\nTechLevel=3\nSight=8\nSpeed=4\nCrateGoodie=yes\nCrusher=yes\nOwner=GDI\nCost=800\nPoints=25\nROT=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={55D141B8-DB94-11d1-AC98-006008055BB5}\nMovementZone=Destroyer\nThreatPosed=40\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nDamageSmokeOffset=100, 100, 275\nWeight=3.5\nEliteAbilities=SENSORS\nAccelerates=false\nZFudgeColumn=8\nZFudgeTunnel=13\nZFudgeBridge=2\n\n; Mammoth Mk. II\n[HMEC]\nName=Mammoth Mk.II\nPrerequisite=GDIFACTORY,GATECH\nPrimary=MechRailgun\nSecondary=MammothTusk\nStrength=1200\t;800\nCategory=AFV\nArmor=heavy\nTechLevel=10\nSight=8\nSpeed=3\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=3000\nTrainable=no\nSelfHealing=yes\nPoints=25\nROT=3\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={55D141B8-DB94-11d1-AC98-006008055BB5}\nMovementZone=Destroyer\nThreatPosed=80\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nDamageSmokeOffset=300, 300, 425\nTiltsWhenCrushes=false\nBuildLimit=1\nWeight=3.5\nAccelerates=false\nZFudgeColumn=12\nZFudgeTunnel=15\nZFudgeBridge=25\n\n; Small Mech\n[SMECH]\nName=Wolverine\nPrerequisite=GDIFACTORY\nPrimary=AssaultCannon\nStrength=175\nCategory=AFV\nArmor=light\nTurret=no\nIsTilter=no\nTooBigToFitUnderBridge=true\nTechLevel=2\nSight=6\nSpeed=7\nCrateGoodie=yes\nOwner=GDI\nCost=500\nPoints=25\nROT=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=15-I000,15-I006,15-I040,15-I042\nVoiceMove=15-I024,15-I044\nVoiceAttack=15-I006,15-I046\nVoiceFeedback=\nMaxDebris=2\nLocomotor={55D141B8-DB94-11d1-AC98-006008055BB5}\nMovementZone=Normal\nThreatPosed=15\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nEliteAbilities=VEIN_PROOF\nAccelerates=false\nImmuneToVeins=yes\n\n; Attack Cycle\n[BIKE]\nName=Attack Cycle\nPrerequisite=NODFACTORY\nPrimary=BikeMissile\nCategory=Recon\nStrength=150\nArmor=wood\nTurret=no\nIsTilter=yes\nTechLevel=5\nSight=5\nSpeed=12\nCrateGoodie=yes\nOwner=Nod\nCost=600\nPoints=25\nROT=8\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=2\nDebrisTypes=TIRE\nDebrisMaximums=2\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Destroyer\nThreatPosed=20\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nElite=HoverMissile\nEliteAbilities=VEIN_PROOF\n\n; Attack Buggy\n[BGGY]\nName=Attack Buggy\nPrerequisite=NODFACTORY\nPrimary=RaiderCannon\nCategory=Recon\nStrength=220\nArmor=light\nTurret=no\nIsTilter=yes\nTechLevel=2\nSight=6\nSpeed=10\nCrateGoodie=yes\nOwner=Nod\nCost=500\nPoints=25\nROT=8\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=3\nDebrisTypes=TIRE\nDebrisMaximums=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Normal\nThreatPosed=10\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nEliteAbilities=CRUSHER\nImmuneToVeins=yes\n\n; Subterranean APC\n[SAPC]\nName=Subterranean APC\nPrerequisite=NODFACTORY,NATECH\nStrength=175\nMoveToShroud=no\nCategory=Transport\nDeployTime=.022\nArmor=heavy\nTurret=no\nIsTilter=yes\nTechLevel=6\nSight=5\nPipScale=Passengers\nSpeed=5\nCrateGoodie=yes\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=800\nPoints=25\nROT=5\nCrusher=yes\nPassengers=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582743-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Subterannean\nThreatPosed=10\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nSpecialThreatValue=1\nZFudgeColumn=7\nZFudgeTunnel=13\n\n; Subterranean Tank\n[SUBTANK]\nName=Devil's Tongue\nPrerequisite=NODFACTORY,NATECH\nPrimary=FireballLauncher\nMoveToShroud=no\nStrength=300\nCategory=AFV\nDeployTime=.022\nTypeImmune=yes\nArmor=light\nTurret=no\nIsTilter=yes\nTechLevel=7\nSight=5\nSpeed=5\nCrateGoodie=yes\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=750\nPoints=25\nROT=6\nCrusher=yes\nNoMovingFire=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582743-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Subterannean\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=SELF_HEAL\nAutoCrush=no\nAccelerates=false\nZFudgeColumn=10\nZFudgeTunnel=14\n\n; Disruptor\n[SONIC]\nName=Disruptor\nPrerequisite=GDIFACTORY,GATECH\nPrimary=SonicZap\nStrength=500\nTypeImmune=yes\nArmor=heavy\nCategory=AFV\nIsTilter=yes\nTechLevel=9\nTurret=yes\nSight=7\nSpeed=4\nCrateGoodie=yes\nOwner=GDI\nCost=1300\nPoints=25\nROT=4\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nAllowedToStartInMultiplayer=no\nMaxDebris=5\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Destroyer\nThreatPosed=60\t; This value MUST be 0 for all building addons\nNoMovingFire=true\t\t; This MUST be set to true for the sonic tank\nDamageParticleSystems=SparkSys,SmallGreySSys\nEliteAbilities=EXPLODES\nZFudgeColumn=12\nZFudgeTunnel=15\n\n; Tick Tank\n[TTNK]\nName=Tick Tank\nCategory=AFV\nPrerequisite=NODFACTORY\nPrimary=90mm\nStrength=350\nArmor=light\nTechLevel=3\nCrateGoodie=yes\nSight=5\nSpeed=6\nOwner=Nod\nCost=800\nPoints=40\nROT=5\nCrusher=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Destroyer\nDeploysInto=GATICK\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=SENSORS\nElite=120mmx\nAccelerationFactor=0.01\nZFudgeColumn=8\nZFudgeTunnel=13\n\n; Stealth tank\n[STNK]\nName=Stealth Tank\nPrerequisite=NODFACTORY,NATECH\nPrimary=Dragon\nStrength=180 ; w250\nArmor=light\nCategory=AFV\nTurret=no\nIsTilter=yes\nTechLevel=8\nSight=5\nSpeed=6\nCrateGoodie=yes\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=1100\nPoints=25\nROT=5\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=3\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nCloakable=yes\nCloakingSpeed=5\nMovementZone=Destroyer\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=EXPLODES\nZFudgeColumn=8\nZFudgeTunnel=13\n\n; Cyborg Reaper\n[REAPER]\nName=Cyborg Reaper\nCategory=AFV\nPrerequisite=NATECH,NODFACTORY\nPrimary=QuadLauncher\nSecondary=WebLauncher\nStrength=400 ;was 350\nArmor=light\nTechLevel=-1\nSight=7\nSpeed=5\nOwner=Nod\nCost=1100\nPoints=30\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nExplosion=REAPRDIE\t;TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=60-N100,60-N102,60-N104\nVoiceMove=60-N106,60-N108,60-N110\nVoiceAttack=60-N112,60-N114,60-N116\nVoiceFeedback=\t;SPIDDIE1\nCrushable=no\nIsTilter=yes\nFireAngle=10\nSpeedType=Creep\nNonVehicle=yes\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\nImmuneToVeins=yes\nTiberiumProof=yes\nTiberiumHeal=yes\nEliteAbilities=CRUSHER\nAccelerates=false\n\n\n; Tiberium Jellyfish\n[JFISH]\nName=Tiberium Floater\nInsignificant=yes\nImage=FLOATER\nNominal=yes\nStrength=500\nCategory=Civilian\nArmor=light\nTechLevel=-1\nSight=5\nSpeed=10\nTiberiumHeal=yes\nTiberiumProof=yes\nImmuneToVeins=yes\nOwner=Civilian\nAllowedToStartInMultiplayer=no\nCost=1\nPoints=50\nROT=16\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nLocomotor={3DC0B295-6546-11D3-80B0-00902792494C}\nMovementZone=AmphibiousDestroyer\nThreatPosed=20\t; This value MUST be 0 for all building addons\nGuardRange=5\nNonVehicle=yes\nJellyfish=yes\nSpeedType=Hover\nPrimary=Tentacle\nCrateGoodie=no\nTrainable=no\n\n; Juggernaut\n[JUGG]\nName=Juggernaut\nCategory=AFV\nPrerequisite=GDIFACTORY,GARADR\nImage=JUGGER\nPrimary=Jugg90mm\nStrength=350\nArmor=light\nTechLevel=-1\nSight=9\nSpeed=5\nOwner=GDI\nCost=950\nPoints=40\nROT=5\nCrusher=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={55D141B8-DB94-11d1-AC98-006008055BB5}\nMovementZone=Destroyer\nDeploysInto=DJUGG\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=SENSORS\nAccelerationFactor=0.01\nNoMovingFire=yes\nDeployToFire=yes\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\n\n\n; Limpet drone mine\n[LIMPET]\nName=Limpet Drone\nImage=LIMPED\nIsLimpetDrone=yes\nOwner=GDI,Nod\nCategory=AFV\nPrerequisite=FACTORY,RADAR\nStrength=100\nCost=550  ;was 700\nSpeed=8\nSight=5\nArmor=none\nPoints=50\nTechLevel=-1\nDeploysInto=DLIMPET\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nThreatPosed=10\t; This value MUST be 0 for all building addons\nGuardRange=0\nSpeedType=Hover\nLocomotor={4A582742-9839-11d1-B709-00A024DDAFD1}\nMovementZone=AmphibiousDestroyer\nAlternateSpeed=10\nCrateGoodie=no\nAllowedToStartInMultiplayer=no\nTrainable=no\nVoiceSelect=LIMPQ3,LIMPQ4\nVoiceMove=LIMPC3,LIMPC4\nVoiceAttack=LIMPC3,LIMPC4\nVoiceFeedback=LIMPC3,LIMPC4\n\n;Mobile EM-Pulse\n[MOBILEMP]\nName=Mobile EM-Pulse\nImage=M_EMP\nPrerequisite=GDIFACTORY,NAPULS\nStrength=800  ;was 600\nCategory=Support\nArmor=heavy\nTechLevel=-1\nSight=6\nSpeed=7  ;was 3\nOwner=GDI\nCost=1000 ;was 1400\nPoints=60\nROT=5\nCrewed=yes\nCrusher=yes\nTypeImmune=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=12\nZFudgeTunnel=15\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\nPipScale=Charge\nMaxCharge=1800  ; was 1200\nStartCharge=0;\nIsMobileEMP=true\nTrainable=no\n\n;Mobile EM-Pulse (Precharged)\n[CMOBILEMP]\nName=Mobile EM-Pulse (Charged)\nImage=M_EMP\nStrength=800\nCategory=Support\nArmor=heavy\nTechLevel=-1\nSight=6\nSpeed=7  ;was 3\nOwner=GDI\nCost=1000\nPoints=60\nROT=5\nCrewed=yes\nCrusher=yes\nTypeImmune=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Crusher\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=12\nZFudgeTunnel=15\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\nPipScale=Charge\nMaxCharge=1800  ; was 1200\nStartCharge=1800;\nIsMobileEMP=true\nTrainable=no\n\n; Mobile Stealth Generator\n[SGEN]\nName=Mobile Stealth Generator\nImage=SGEN\nPrerequisite=NODFACTORY,NASTLH\nStrength=200  ;was 250\nArmor=light\nCategory=AFV\nTurret=no\nIsTilter=yes\nTechLevel=-1\nSight=5\nSpeed=6\nOwner=Nod\nCost=1600  ;was 1800\nPoints=25\nROT=5\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=3\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Crusher\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=EXPLODES\nZFudgeColumn=8\nZFudgeTunnel=13\n;PipScale=Charge\n;MaxCharge=400\nDeploysInto=MSTL\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\nTrainable=no\nCrewed=no\n\n; Mobile Weapons Factory (GDI)\n[MOBWARG]\nName=Mobile War Factory\nImage=MWAR_NOD\nPrerequisite=GAWEAP,GAPLUG\nBuildLimit=1\nTechLevel=-1\nCategory=Support\nStrength=800\nArmor=heavy\nSight=6\nSpeed=3\nOwner=GDI\nCost=1800\nPoints=60\nROT=5\nCrewed=yes\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=12\nZFudgeTunnel=15\nDeploysInto=DGWEAP\nCrateGoodie=no\nAllowedToStartInMultiplayer=no\nTrainable=no\n\n; Mobile Weapons Factory (Nod)\n[MOBWARN]\nName=Fist of Nod\nImage=MWAR_NOD\nPrerequisite=NAWEAP,NATMPL\nBuildLimit=1\nTechLevel=-1\nCategory=Support\nStrength=800\nArmor=heavy\nSight=6\nSpeed=3\nOwner=Nod\nCost=1800\nPoints=60\nROT=5\nCrewed=yes\nCrusher=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=6\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nWeight=3.5\nMovementZone=Normal\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nSpecialThreatValue=1\nZFudgeColumn=12\nZFudgeTunnel=15\nDeploysInto=DNWEAP\nCrateGoodie=no\nAllowedToStartInMultiplayer=no\nTrainable=no\n\n; Retro Flame Tank\n[FLMTNK]\nName=Flame Tank\nImage=FTNK\nCategory=AFV\nPrerequisite=NODFACTORY\nPrimary=FireballLauncher\nStrength=300\nArmor=light\nTechLevel=-1\nCrateGoodie=yes\nAllowedToStartInMultiplayer=no\nSight=5\nSpeed=6\nOwner=Civilian\nCost=700\nPoints=40\nROT=5\nCrusher=yes\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nVoiceFeedback=\nMaxDebris=4\nLocomotor={4A582741-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Destroyer\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nWeight=3.5\nEliteAbilities=EXPLODES\nAccelerationFactor=0.01\nZFudgeColumn=8\nZFudgeTunnel=13\n\n; Core Defender\n[DEFENDER]\nName=Core Defender\nCategory=AFV\nPrerequisite=\nStrength=10000 ;was 2500\nArmor=heavy\nTechLevel=-1\nSight=9\nSpeed=5\nOwner=Civilian\nCost=2000\nPoints=40\nROT=5\nCrusher=yes\nCrewed=no\nWeight=3.5\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nLocomotor={55D141B8-DB94-11d1-AC98-006008055BB5}\nMovementZone=Destroyer\nThreatPosed=50\nDamageParticleSystems=SparkSys,SmallGreySSys\nMaxDebris=30  ;was 10\nExplosion=DEFD_EXP\nAllowedToStartInMultiplayer=no\nIsCoreDefender=yes\nWalkRate=4\nPrimary=DEFOB\nDamageSmokeOffset=0,0,550\nTrainable=yes\nEliteAbilities=SENSORS\nImmuneToVeins=yes\nTiberiumProof=yes\nTiberiumHeal=yes\nSelfHealing=yes\nNoMovingFire=true\n\n\n\n\n; ******* Infantry Types *******\n; This is the list of infantry types. Each infantry type listed\n; here should also have a matching data section that specifies\n; its data values. The purpose of this list is to identify infantry\n; types that can't be implicitly determined by examining other\n; entries in this rules file.\n[InfantryTypes]\n00=E1\n01=E2\n02=E3\n03=MEDIC\n04=ELCAD\n05=CTECH\n06=HUEY\n07=ENGINEER\t;index 7 is used in nod5a.map\n08=CYBORG\n09=JUMPJET\n10=CHAMSPY\n11=UMAGON\n12=GHOST\n13=CYC2\n14=MHIJACK\n15=CIV1\n16=CIV2\n17=CIV3\n18=CIV4\n19=CIV5\n20=CIV6\n21=MUTANT\n22=MWMN\n23=MUTANT3\n24=TRATOS\n25=OXANNA\n26=SLAV\n27=DOGGIE\n;28=WEEDGUY\n\n[CYC2]\nName=Cyborg Commando\nCategory=Soldier\nPrimary=CyCannon\n;Secondary=FireballLauncher\nPrerequisite=NAHAND,NATMPL\nCrushSound=SQUISHY2\nCrushable=no\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=500\nFearless=yes\nArmor=heavy\nTechLevel=10\nSight=7\nPip=white\nSpeed=4\nOwner=Nod\nCost=2000\nTrainable=no\nCyborg=yes\nPoints=5\nAllowedToStartInMultiplayer=no\nVoiceSelect=23-I000,23-I002,23-I004,23-I006\nVoiceMove=23-I008,23-I010,23-I012,23-I016\nVoiceAttack=23-I014,23-I018,23-I020,23-I022\nVoiceFeedback=\nVoiceDie=22-N104,22-N106,22-N108\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=50\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys\nBuildLimit=1\nImmuneToVeins=yes\nIsWebImmune=true\n\n; Chameleon Spy\n[CHAMSPY]\nName=Chameleon Spy\nCategory=Soldier\nPrerequisite=NAHAND,NATECH\nCrushSound=SQUISH6\nStrength=120\nArmor=none\nTechLevel=-1\nAgent=yes\nSight=9\nSpeed=6\nInfiltrate=yes\nOwner=Nod\nAllowedToStartInMultiplayer=no\nCost=700\nPip=white\nPoints=5\nVoiceSelect=21-I000,21-I002,21-I004\nVoiceMove=21-I010,21-I012,21-I016\nVoiceAttack=21-I010,21-I012,21-I022\nVoiceFeedback=21-I000,21-I002\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nCloakable=yes\nCloakingSpeed=10\nThreatPosed=0\t; This value MUST be 0 for all building addons\nSpecialThreatValue=1\nImmuneToVeins=yes\n\n; rifle soldier\n[E1]\nName=Light Infantry\nCategory=Soldier\nPrimary=Minigun\nPrerequisite=BARRACKS\nCrushSound=SQUISH6\nStrength=125\nPip=green\nArmor=none\nTechLevel=1\nSight=5\nSpeed=5\nOwner=GDI,Nod\nCost=120\nPoints=5\nVoiceSelect=15-I000,15-I004,15-I012,15-I048\nVoiceMove=15-I018,15-I024,15-I044\nVoiceAttack=15-I044,15-I050,15-I044,15-I046\nVoiceFeedback=15-I058,15-I064\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nElite=M1Carbine\nEliteAbilities=SCATTER\nImmuneToVeins=yes\n\n[E2]\nName=Disc Thrower\nCategory=Soldier\nPrimary=Grenade\nPrerequisite=GAPILE\nCrushSound=SQUISH6\nStrength=150\nArmor=none\nTechLevel=2\nPip=green\nSight=7\nSpeed=4\nOwner=GDI\nCost=200\nPoints=5\nVoiceSelect=15-I000,15-I004,15-I012,15-I048\nVoiceMove=15-I018,15-I024,15-I044\nVoiceAttack=15-I044,15-I050,15-I044,15-I046\nVoiceFeedback=15-I058,15-I064\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=InfantryDestroyer\nThreatPosed=15\t; This value MUST be 0 for all building addons\nEliteAbilities=SCATTER\nImmuneToVeins=yes\nExplodes=yes\n\n[E3]\nName=Rocket Infantry\nCategory=Soldier\nPrimary=BAZOOKA\nPrerequisite=NAHAND\nCrushSound=SQUISH6\nStrength=100\nArmor=none\nTechLevel=2\nPip=green\nSight=7\nSpeed=4\nOwner=Nod\nCost=250\nPoints=5\nVoiceSelect=15-I000,15-I032,15-I048\nVoiceMove=15-I008,15-I014,15-I026\nVoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060\nVoiceFeedback=15-I058,15-I064\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=InfantryDestroyer\nThreatPosed=20\t; This value MUST be 0 for all building addons\nEliteAbilities=SCATTER\nImmuneToVeins=yes\n\n;; Hack to make MultiMissile & MobileEMPulseWeapon work! Don't change data!\n;[WEEDGUY]\n;Name=Chem Spray Infantry\n;Category=Soldier\n;Primary=MultiCluster\n;Secondary=DualRockets\n;Elite=MobileEMPulseWeapon\n;Prerequisite=BARRACKS\n;TiberiumProof=yes\n;CrushSound=SQUISHY2\n;Strength=130\n;Storage=7\n;Pip=green\n;Fearless=yes\n;Armor=none\n;TechLevel=-1\n;Sight=4\n;Speed=3\n;Owner=GDI\n;AllowedToStartInMultiplayer=no\n;Cost=300\n;Points=5\n;VoiceSelect=15-I000,15-I006,15-I040,15-I042\n;VoiceMove=15-I024,15-I044\n;VoiceAttack=15-I006,15-I046\n;VoiceFeedback=15-I058,15-I064\n;VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\n;Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\n;PhysicalSize=1\n;MovementZone=Infantry\n;ThreatPosed=0\t; This value MUST be 0 for all building addons\n\n[MEDIC]\nName=Medic\nCategory=Soldier\nPrimary=Heal\nPrerequisite=GAPILE\nCrushSound=SQUISHY2\nStrength=125\nArmor=none\nTechLevel=4\nSight=6\nSpeed=4\nSelfHealing=yes\nPip=red\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=600\nPoints=5\nVoiceSelect=20-I000,20-I004,20-I006\nVoiceMove=20-I008,20-I010,20-I012\nVoiceAttack=20-I016,20-I018,20-I020\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nGuardRange=8\nSpecialThreatValue=1\nImmuneToVeins=yes\n\n[UMAGON]\nName=Umagon\nCategory=Soldier\nPrimary=Sniper\nCrushSound=SQUISH6\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=150\nArmor=light\nTechLevel=-1\nSight=7\nSpeed=5\nOwner=GDI\nPip=white\nAllowedToStartInMultiplayer=no\nCost=400\nPoints=5\nTrainable=no\nVoiceSelect=10-I000,10-I002,10-I004,10-I006\nVoiceMove=10-I016,10-I020,10-I022\nVoiceAttack=10-I024,10-I026,10-I028,10-I030\nVoiceFeedback=\nVoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=15\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[GHOST]\nName=Ghost Stalker\nCategory=Soldier\nPrerequisite=GAPILE,GATECH\nPrimary=LtRail\nC4=yes\nTiberiumHeal=yes\nCrushSound=SQUISHY2\nTiberiumProof=yes\nStrength=200\nArmor=light\nTechLevel=10\nPip=white\nSight=6\nSpeed=4\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=1750\nPoints=5\nTrainable=no\nVoiceSelect=14-I000,14-I002,14-I004\nVoiceMove=14-I008,14-I010,14-I012,14-I014\nVoiceAttack=14-I008,14-I010,14-I014,14-I016\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=25\t; This value MUST be 0 for all building addons\nSpecialThreatValue=1\nBuildLimit=1\nImmuneToVeins=yes\n\n[CYBORG]\nName=Cyborg\nCategory=Soldier\nPrerequisite=NAHAND\nPrimary=Vulcan3\nCrushSound=SQUISHY2\nCrushable=no\nTiberiumProof=yes\nTiberiumHeal=yes\nFearless=yes\nCyborg=yes\nPip=white\nStrength=300 ; w350\nArmor=light\nTechLevel=4\nSight=5\nSpeed=4\nOwner=Nod\nCost=650\nPoints=5\nVoiceSelect=22-I000,22-I002,22-I006\nVoiceMove=22-I008,22-I010,22-I014,22-I016,22-I020\nVoiceAttack=22-I008,22-I010,22-I012,22-I018\nVoiceFeedback=\nVoiceDie=22-N104,22-N106,22-N108\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nDamageParticleSystems=SparkSys\nThreatPosed=15\t; This value MUST be 0 for all building addons\nEliteAbilities=STRONGER\nImmuneToVeins=yes\n\n;Nod Elite Cadre Soldier\n[ELCAD]\nName=Elite Cadre\nCategory=Soldier\nImage=SLAV\nPrimary=Vulcan3\nPrerequisite=NAHAND\nTiberiumProof=yes\nCrushSound=SQUISH6\nStrength=175\nFearless=yes\nArmor=light\nPip=white\nTechLevel=-1\nSight=4\nSpeed=4\nOwner=Nod\nCost=300\nAllowedToStartInMultiplayer=no\nPoints=5\nVoiceSelect=15-I032,15-I048\nVoiceMove=15-I008,15-I014,15-I026\nVoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[MUTANT]\nName=Mutant\nCategory=Soldier\nPrimary=Vulcan\nCrushSound=SQUISH6\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=50\nArmor=none\nTechLevel=-1\nSight=4\nSpeed=4\nOwner=GDI,Nod\nCost=100\nPip=white\nPoints=5\nAllowedToStartInMultiplayer=no\nVoiceSelect=15-I032,15-I048\nVoiceMove=15-I008,15-I014,15-I026\nVoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[MWMN]\nName=Mutant Soldier\nCategory=Soldier\nPrimary=Vulcan\nCrushSound=SQUISH6\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=50\nArmor=none\nTechLevel=-1\nSight=4\nSpeed=4\nPip=white\nOwner=GDI,Nod\nCost=100\nPoints=5\nAllowedToStartInMultiplayer=no\nVoiceSelect=11-I000,11-I002,11-I004,11-I006\nVoiceMove=11-I008,11-I010,11-I012\nVoiceAttack=11-I012,11-I010,11-I016\nVoiceFeedback=\nVoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[MUTANT3]\nName=Mutant Sergeant\nCategory=Soldier\nPrimary=Vulcan\nCrushSound=SQUISHY2\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=50\nPip=white\nArmor=none\nTechLevel=-1\nSight=4\nSpeed=4\nOwner=GDI,Nod\nCost=100\nPoints=5\nAllowedToStartInMultiplayer=no\nVoiceSelect=15-I032,15-I048\nVoiceMove=15-I008,15-I014,15-I026\nVoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[TRATOS]\nName=Tratos\nCategory=Soldier\nPrimary=none\nCrushSound=SQUISHY2\nTiberiumProof=yes\nTiberiumHeal=yes\nStrength=200\nArmor=none\nTechLevel=-1\nSight=4\nSpeed=5\nOwner=GDI,Nod\nPip=white\nCost=100\nPoints=5\nAllowedToStartInMultiplayer=no\nVoiceSelect=13-I000,13-I002,13-I004,13-I006\nVoiceMove=13-I008,13-I010,13-I012,13-I014\nVoiceAttack=13-I016,13-I018,13-I020\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[OXANNA]\nName=Oxanna\nCategory=Soldier\nPrimary=Vulcan\nCrushSound=SQUISH6\n;TiberiumProof=yes\t;Crim: this has gotta be an oversight\n;TiberiumHeal=yes\nStrength=50\nArmor=none\nTechLevel=-1\nSight=4\nSpeed=4\nAllowedToStartInMultiplayer=no\nOwner=GDI,Nod\nCost=100\nPip=white\nPoints=5\nVoiceSelect=11-I000,11-I002,11-I004,11-I006\nVoiceMove=11-I008,11-I010,11-I012\nVoiceAttack=11-I014,11-I016,11-I018\nVoiceFeedback=\nVoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[DOGGIE]\nName=Tiberian Fiend\nNominal=yes\nCategory=Soldier\nDoggie=yes\nPrimary=FiendShard\nCrushSound=SQUISHY2\nStrength=250\nArmor=light\nFearless=no\nTiberiumProof=yes\nTiberiumHeal=yes\nTrainable=no\nPip=green\nTechLevel=-1\nSight=4\nSpeed=8\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=100\nPoints=5\nVoiceSelect=\nVoiceMove=\nVoiceAttack=\nVoiceFeedback=\nVoiceDie=FIEND1\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=25\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n[ENGINEER]\nName=Engineer\nCategory=Soldier\nPrimary=none\nPrerequisite=BARRACKS\nCrushSound=SQUISH6\nStrength=100\nArmor=none\nTechLevel=2\nSight=4\nSpeed=4\nPip=yellow\nEngineer=yes\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=500\nPoints=5\nVoiceSelect=19-I000,19-I002,19-I006\nVoiceMove=19-I010,19-I016\nVoiceAttack=19-I018,19-I016\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nSpecialThreatValue=1\t; this should be between 0 and 1\nImmuneToVeins=yes\nGuardRange=9\n\n[JUMPJET]\nName=Jumpjet Infantry\nCategory=Soldier\nJumpJet=yes\nPrimary=JumpCannon\nPrerequisite=GAPILE,GARADR\nCrushable=no\nStrength=120\nFearless=yes\nArmor=light\nTechLevel=6\nSight=6\nPip=green\nSpeed=5\nOwner=GDI\nAllowedToStartInMultiplayer=no\nCost=600\nPoints=5\nVoiceSelect=15-I000,15-I004,15-I012,15-I048\nVoiceMove=15-I018,15-I024,15-I044\nVoiceAttack=15-I044,15-I050,15-I044,15-I046\nVoiceFeedback=15-I058,15-I064\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={92612C46-F71F-11d1-AC9F-006008055BB5}\nPhysicalSize=1\nMovementZone=Fly\t\t; This needs to be None, like aircraft\nThreatPosed=15\t; This value MUST be 0 for all building addons\nEliteAbilities=RADAR_INVISIBLE\n\n[MHIJACK]\nName=Mutant Hijacker\nCategory=Soldier\nPrerequisite=NAHAND,NATMPL\nPrimary=none\nCrushable=no\n;CrushSound=SQUISHY2\nStrength=300\nArmor=none\nTiberiumProof=yes\nTiberiumHeal=yes\nTechLevel=10\nSight=6\nSpeed=7\nPip=white\nOwner=Nod\nCost=1850\nAllowedToStartInMultiplayer=no\nPoints=5\nTrainable=no\nVoiceSelect=24-I000,24-I002,24-I004,24-I006\nVoiceMove=24-I008,24-I010,24-I012,24-I014\nVoiceAttack=24-I016,24-I018,24-I020,24-I022,24-I024\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nVehicleThief=yes\nMovementZone=Infantry\nThreatPosed=20\t; This value MUST be 0 for all building addons\nSpecialThreatValue=1\t; this should be between 0 and 1\nGuardRange=6\nBuildLimit=1\nImmuneToVeins=yes\n\n[SLAV]\nName=Slavick\nCategory=Soldier\nPrimary=none\nCrushSound=SQUISH6\nStrength=300\nArmor=none\nPip=white\nTechLevel=-1\nSight=4\nSpeed=4\nOwner=GDI,Nod\nCost=100\nAllowedToStartInMultiplayer=no\nPoints=5\nVoiceSelect=12-I000,12-I002,12-I004\nVoiceMove=12-I006,12-I008,12-I010\nVoiceAttack=12-I012,12-I014,12-I016\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=10\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\n\n; civilians\n\n[CIV1]\nName=Civilian\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=yes\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=67-N100,67-N102\nVoiceMove=67-N104,67-N106,67-N108\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[CIV2]\nName=Civilian\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISHY2\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=yes\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=68-N100,68-N102,68-N104\nVoiceMove=68-N106,68-N108,68-N110\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[CIV3]\nName=Civilian\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=yes\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=69-N100,69-N102,69-N104\nVoiceMove=69-N106,69-N108,69-N110\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[CIV4]\nName=Civilian\nImage=CIV1\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=no\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=67-N100,67-N102\nVoiceMove=67-N104,67-N106,67-N108\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n\n[CIV5]\nName=Civilian\nImage=CIV2\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=no\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=68-N100,68-N102,68-N104\nVoiceMove=68-N106,68-N108,68-N110\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n\n[CIV6]\nName=Civilian\nImage=CIV3\nCategory=Civilian\nStrength=50\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\n;Ammo=10\nFraidycat=no\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=69-N100,69-N102,69-N104\nVoiceMove=69-N106,69-N108,69-N110\nVoiceAttack=BOOP\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[CTECH]\nName=Technician\nImage=CIV3\nCategory=Civilian\nStrength=50\nPrimary=Pistola\nArmor=none\nTechLevel=-1\nCrushSound=SQUISH6\nInsignificant=yes\nSight=2\nSpeed=5\nOwner=GDI,Nod\nAllowedToStartInMultiplayer=no\nCost=10\nPoints=1\nAmmo=10\nReload=80\nFraidycat=no\nCivilian=yes\nNominal=yes\nPip=white\nVoiceSelect=70-N000,70-N002,70-N004\nVoiceMove=70-N006,70-N008,70-N010\nVoiceAttack=70-N014,70-N016,70-N018\nVoiceFeedback=\nVoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nThreatPosed=0\t; This value MUST be 0 for all building addons\nImmuneToVeins=yes\nEliteAbilities=SCATTER\n\n[HUEY]\nName=Huey the Infected Cyborg\nImage=CYBORG\nCategory=Soldier\nPrerequisite=\nPrimary=Vulcan3\nCrushSound=SQUISHY2\nCrushable=no\nTiberiumProof=yes\nTiberiumHeal=yes\nFearless=yes\nCyborg=yes\nPip=white\nStrength=300 ; w350\nArmor=light\nTechLevel=-1\nSight=5\nSpeed=4\nOwner=\nCost=650\nPoints=5\nVoiceSelect=22-I000,22-I002,22-I006\nVoiceMove=22-I008,22-I010,22-I014,22-I016,22-I020\nVoiceAttack=22-I008,22-I010,22-I012,22-I018\nVoiceFeedback=\nVoiceDie=22-N104,22-N106,22-N108\t;Crim: was DEDMAN1...6, not fitting\nLocomotor={4A582744-9839-11d1-B709-00A024DDAFD1}\nPhysicalSize=1\nMovementZone=Infantry\nDamageParticleSystems=SparkSys\nThreatPosed=15\t; This value MUST be 0 for all building addons\nEliteAbilities=STRONGER\nImmuneToVeins=yes\n\n\n\n; ******* Aircraft Types *******\n; This lists all of the aircraft types in the game. Each aircraft\n; type should have a matching section that specifies the data it\n; requires.\n[AircraftTypes]\n0=ORCA\n1=APACHE\n2=TRNSPORT\n3=ORCAB\n4=SCRIN\n5=DPOD\n6=ORCATRAN\n7=DSHP\n\n; Drop Pod\n[DPOD]\nName=Drop Pod\nCategory=AirPower\nPrerequisite=afld\nPrimary=Vulcan2\nStrength=60\nSelectable=no\nArmor=light\nTechLevel=-1\nSight=0\nRadarInvisible=yes\nSpeed=16\nOwner=GDI\nCost=10\nPoints=20\nROT=5\nAmmo=5\nPassengers=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I030,30-I034,30-I036\nLocomotor={4A582745-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=10\t; This value MUST be 0 for all building addons\n\n; Dropship\n[DSHP]\nName=Dropship\nPrerequisite=GAWEAP\nStrength=200\nCategory=AirLift\nArmor=heavy\nLandable=yes\nTechLevel=-1\nSight=3\nPipScale=Passengers\nSpeed=18\nPitchSpeed=.4\nRadarInvisible=yes\nOwner=GDI\nCost=0\nPoints=25\nROT=5\nSelectable=yes\nPassengers=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I034,30-I036\nIsDropship=yes\nFlightLevel=1600\nMaxDebris=9\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=0\t; This value MUST be 0 for all building addons\n;Dock=GADROP\nSlowdownDistance=2000\nDamageParticleSystems=SparkSys,SmallGreySSys\nLegalTarget=no\nAuxSound1=DROPUP1\t;Taking off\nAuxSound2=DROPDWN1\t;Landing\n\n; ORCA Fighter\n[ORCA]\nName=Orca Fighter\nPrerequisite=GAHPAD\nPrimary=Hellfire\nStrength=200\nCategory=AirPower\nArmor=light\nTechLevel=5\nSight=2\nRadarInvisible=no\nLandable=yes\nMoveToShroud=no\nDock=GAHPAD,NAHPAD\nPipScale=Ammo\nSpeed=20\nPitchSpeed=.16\nOwner=GDI\nCost=1000\nPoints=20\nROT=5\nAmmo=5\nCrewed=yes\nGuardRange=30\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I030,30-I034,30-I036\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=20\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nAuxSound1=ORCAUP1\t;Taking off\nAuxSound2=ORCADWN1\t;Landing\n\n; ORCA Bomber\n[ORCAB]\nName=Orca Bomber\nPrerequisite=GAHPAD,GATECH\nPrimary=Bomb\nStrength=260\nCategory=AirPower\nArmor=light\nTechLevel=8\nSight=2\nRadarInvisible=no\nLandable=yes\nMoveToShroud=no\nDock=GAHPAD,NAHPAD\nPipScale=Ammo\nSpeed=12\nPitchSpeed=.16\nOwner=GDI\nCost=1600\nPoints=20\nROT=5\nAmmo=2\nCrewed=yes\nGuardRange=30\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I030,30-I034,30-I036\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=25\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nAuxSound1=ORCAUP1\t;Taking off\nAuxSound2=ORCADWN1\t;Landing\nEliteAbilities=RADAR_INVISIBLE\n\n; Orca Transport\n[ORCATRAN]\nName=Orca Transport\nPrerequisite=GAHPAD\nStrength=200\nCategory=AirPower\nArmor=light\nTechLevel=-1\nSight=2\nRadarInvisible=no\nLandable=yes\nPipScale=Passengers\nPassengers=5\nSpeed=9\nPitchSpeed=1.1\nOwner=GDI\nCost=1200\nPoints=20\nROT=5\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I034,30-I036\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nDamageParticleSystems=SparkSys,SmallGreySSys\nAuxSound1=ORCAUP1\t;Taking off\nAuxSound2=ORCADWN1\t;Landing\nThreatPosed=0\nSpecialThreatValue=1\n\n; Carryall\n[TRNSPORT]\nName=Carryall\nPrerequisite=GAHPAD,GADEPT\nStrength=175\nCategory=AirPower\nArmor=light\nTechLevel=9\nSight=2\nRadarInvisible=no\nCarryall=yes\nLandable=yes\nMoveToShroud=no\nSpeed=16\nPitchSpeed=1.1\nOwner=GDI\nCost=750\nPoints=20\nROT=5\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I034,30-I036\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nDamageParticleSystems=SparkSys,SmallGreySSys\nAuxSound1=DROPUP1 ;Taking off\nAuxSound2=DROPDWN1 ;Landing\nThreatPosed=0\nSpecialThreatValue=1\n\n; Banshee Fighter\n[SCRIN]\nName=Banshee\nPrerequisite=NAHPAD,NATECH\nPrimary=Proton\nStrength=280\nCategory=AirPower\nArmor=light\nTechLevel=9\nSight=2\nRadarInvisible=no\nLandable=yes\nMoveToShroud=no\nDock=NAHPAD,GAHPAD\nPipScale=Ammo\nSpeed=18\nPitchSpeed=.9\nOwner=Nod\nCost=1500\nPoints=20\nROT=3\nAmmo=3\nCrewed=yes\nGuardRange=30\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nVoiceSelect=32-I000\nVoiceMove=32-I004\nVoiceAttack=32-I002,32-I004,32-I006\nVoiceFeedback=32-I008\nVoiceDie=32-I008\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nAuxSound1=DROPUP1 ;Taking off\nAuxSound2=DROPDWN1 ;Landing\nEliteAbilities=RADAR_INVISIBLE\n\n; Apache Chopper\n[APACHE]\nName=Harpy\nPrerequisite=NAHPAD\nPrimary=HarpyClaw\nStrength=225\nCategory=AirPower\nArmor=light\nTechLevel=5\nSight=2\nRadarInvisible=yes\nLandable=yes\nMoveToShroud=no\nDock=NAHPAD,GAHPAD\nPipScale=Ammo\nSpeed=14\nPitchSpeed=.16\nOwner=Nod\nCost=1000\nPoints=20\nROT=5\nAmmo=12\nCrewed=yes\nGuardRange=30\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nVoiceSelect=30-I000,30-I002,30-I004,30-I006\nVoiceMove=30-I014,30-I016,30-I018,30-I022\nVoiceAttack=30-I022,30-I030,30-I034,30-I036\nLocomotor={4A582746-9839-11d1-B709-00A024DDAFD1}\nMovementZone=Fly\nThreatPosed=15\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\n\n\n\n; ******* Building Types *******\n; This lists all the buildings types in the game. Each of these\n; types will have a specific section in this file that gives the\n; particulars about that building type.\n[BuildingTypes]            ; Least threat, highest threat, nearest, farthest\n000=GAPOWR                 ; 000, 65536, 131072, 196608\n001=PROC                   ; 001, 65537, 131073, 196609 - 131073 used in ai(fs).ini\n002=GASILO                 ; 002, 65538, 131074, 196610 - 131074 used in ai(fs).ini\n003=GAPILE                 ; 003, 65539, 131075, 196611\n004=GAPLUG                 ; 004, 65540, 131076, 196612 - 131076 used in ai(fs).ini\n005=GACTWR                 ; 005, 65541, 131077, 196613\n006=GAVULC                 ; 006, 65542, 131078, 196614\n007=GASAND                 ; 007, 65543, 131079, 196615\n008=GAFIRE                 ; 008, 65544, 131080, 196616\n009=GADEPT                 ; 009, 65545, 131081, 196617\n010=GATECH                 ; 010, 65546, 131082, 196618\n011=GAWEAP                 ; 011, 65547, 131083, 196619\n012=GACNST                 ; 012, 65548, 131084, 196620 - 131084 used in ai(fs).ini\n013=GAHPAD                 ; 013, 65549, 131085, 196621\n014=NAPOWR                 ; 014, 65550, 131086, 196622\n015=NATECH                 ; 015, 65551, 131087, 196623\n016=NAHAND                 ; 016, 65552, 131088, 196624\n017=NAAPWR                 ; 017, 65553, 131089, 196625\n018=GAWALL                 ; 018, 65554, 131090, 196626\n019=CABHUT                 ; 019, 65555, 131091, 196627\n020=NAPULS                 ; 020, 65556, 131092, 196628\n021=GAGATE_A               ; 021, 65557, 131093, 196629\n022=GAGATE_B               ; 022, 65558, 131094, 196630\n023=NAWEAP                 ; 023, 65559, 131095, 196631\n024=NASTLH                 ; 024, 65560, 131096, 196632\n025=GALITE                 ; 025, 65561, 131097, 196633\n026=REDLAMP                ; 026, 65562, 131098, 196634\n027=GRENLAMP               ; 027, 65563, 131099, 196635\n028=BLUELAMP               ; 028, 65564, 131100, 196636\n029=YELWLAMP               ; 029, 65565, 131101, 196637\n030=PURPLAMP               ; 030, 65566, 131102, 196638\n031=INORANLAMP             ; 031, 65567, 131103, 196639\n032=INGRNLMP               ; 032, 65568, 131104, 196640\n033=INREDLMP               ; 033, 65569, 131105, 196641\n034=NAWALL                 ; 034, 65570, 131106, 196642\n035=INBLULMP               ; 035, 65571, 131107, 196643\n036=NATMPL                 ; 036, 65572, 131108, 196644\n037=NAGATE_A               ; 037, 65573, 131109, 196645\n038=NAGATE_B               ; 038, 65574, 131110, 196646\n039=NAWAST                 ; 039, 65575, 131111, 196647\n040=NAOBEL                 ; 040, 65576, 131112, 196648\n041=NAMISL                 ; 041, 65577, 131113, 196649 - 131113 used in ai(fs).ini\n042=GAPOWRUP               ; 042, 65578, 131114, 196650\n043=NAPOST                 ; 043, 65579, 131115, 196651\n044=NAFNCE                 ; 044, 65580, 131116, 196652\n045=NALASR                 ; 045, 65581, 131117, 196653\n046=NASAM                  ; 046, 65582, 131118, 196654\n047=CITY01                 ; 047, 65583, 131119, 196655\n048=CITY02                 ; 048, 65584, 131120, 196656\n049=CITY03                 ; 049, 65585, 131121, 196657\n050=CITY04                 ; 050, 65586, 131122, 196658\n051=CITY05                 ; 051, 65587, 131123, 196659\n052=CITY06                 ; 052, 65588, 131124, 196660\n053=CITY07                 ; 053, 65589, 131125, 196661\n054=CITY08                 ; 054, 65590, 131126, 196662\n055=CITY09                 ; 055, 65591, 131127, 196663\n056=CITY10                 ; 056, 65592, 131128, 196664\n057=CITY11                 ; 057, 65593, 131129, 196665\n058=CITY12                 ; 058, 65594, 131130, 196666\n059=CITY13                 ; 059, 65595, 131131, 196667\n060=CITY14                 ; 060, 65596, 131132, 196668\n061=CITY15                 ; 061, 65597, 131133, 196669\n062=CITY16                 ; 062, 65598, 131134, 196670\n063=CITY17                 ; 063, 65599, 131135, 196671\n064=CITY18                 ; 064, 65600, 131136, 196672\n065=CAHOSP                 ; 065, 65601, 131137, 196673\n066=GASPOT                 ; 066, 65602, 131138, 196674\n067=CTDAM                  ; 067, 65603, 131139, 196675\n068=NARADR                 ; 068, 65604, 131140, 196676\n069=GAROCK                 ; 069, 65605, 131141, 196677\n070=INGALITE               ; 070, 65606, 131142, 196678\n071=INYELWLAMP             ; 071, 65607, 131143, 196679\n072=INPURPLAMP             ; 072, 65608, 131144, 196680\n073=GAPLUG1                ; 073, 65609, 131145, 196681\n074=GAPLUG2                ; 074, 65610, 131146, 196682\n075=GAPLUG3                ; 075, 65611, 131147, 196683\n076=GAFSDF                 ; 076, 65612, 131148, 196684\n077=GARADR                 ; 077, 65613, 131149, 196685\n078=BBOARD01               ; 078, 65614, 131150, 196686\n079=BBOARD02               ; 079, 65615, 131151, 196687\n080=BBOARD03               ; 080, 65616, 131152, 196688\n081=BBOARD04               ; 081, 65617, 131153, 196689\n082=BBOARD05               ; 082, 65618, 131154, 196690\n083=BBOARD06               ; 083, 65619, 131155, 196691\n084=BBOARD07               ; 084, 65620, 131156, 196692\n085=BBOARD08               ; 085, 65621, 131157, 196693\n086=BBOARD09               ; 086, 65622, 131158, 196694\n087=BBOARD10               ; 087, 65623, 131159, 196695\n088=BBOARD11               ; 088, 65624, 131160, 196696\n089=BBOARD12               ; 089, 65625, 131161, 196697\n090=BBOARD13               ; 090, 65626, 131162, 196698\n091=BBOARD14               ; 091, 65627, 131163, 196699\n092=BBOARD15               ; 092, 65628, 131164, 196700\n093=BBOARD16               ; 093, 65629, 131165, 196701\n094=NEGLAMP                ; 094, 65630, 131166, 196702\n095=NEGRED                 ; 095, 65631, 131167, 196703\n096=ABAN01                 ; 096, 65632, 131168, 196704\n097=ABAN02                 ; 097, 65633, 131169, 196705\n098=ABAN03                 ; 098, 65634, 131170, 196706\n099=ABAN04                 ; 099, 65635, 131171, 196707\n100=ABAN05                 ; 100, 65636, 131172, 196708\n101=ABAN06                 ; 101, 65637, 131173, 196709\n102=ABAN07                 ; 102, 65638, 131174, 196710\n103=ABAN08                 ; 103, 65639, 131175, 196711\n104=ABAN09                 ; 104, 65640, 131176, 196712\n105=ABAN10                 ; 105, 65641, 131177, 196713\n106=ABAN11                 ; 106, 65642, 131178, 196714\n107=ABAN12                 ; 107, 65643, 131179, 196715\n108=ABAN13                 ; 108, 65644, 131180, 196716\n109=ABAN14                 ; 109, 65645, 131181, 196717\n110=ABAN15                 ; 110, 65646, 131182, 196718\n111=ABAN16                 ; 111, 65647, 131183, 196719\n112=ABAN17                 ; 112, 65648, 131184, 196720\n113=ABAN18                 ; 113, 65649, 131185, 196721\n114=CITY19                 ; 114, 65650, 131186, 196722\n115=CITY20                 ; 115, 65651, 131187, 196723\n116=CITY21                 ; 116, 65652, 131188, 196724\n117=NTPYRA                 ; 117, 65653, 131189, 196725\n118=CITY22                 ; 118, 65654, 131190, 196726\n119=CTVEGA                 ; 119, 65655, 131191, 196727\n120=GADPSA                 ; 120, 65656, 131192, 196728\n121=CA0001                 ; 121, 65657, 131193, 196729\n122=CA0002                 ; 122, 65658, 131194, 196730\n123=CA0003                 ; 123, 65659, 131195, 196731\n124=CA0004                 ; 124, 65660, 131196, 196732\n125=CA0005                 ; 125, 65661, 131197, 196733\n126=CA0006                 ; 126, 65662, 131198, 196734\n127=CA0007                 ; 127, 65663, 131199, 196735\n128=CA0008                 ; 128, 65664, 131200, 196736\n129=CA0009                 ; 129, 65665, 131201, 196737\n130=CA0010                 ; 130, 65666, 131202, 196738\n131=CA0011                 ; 131, 65667, 131203, 196739\n132=CA0012                 ; 132, 65668, 131204, 196740\n133=CA0013                 ; 133, 65669, 131205, 196741\n134=CA0014                 ; 134, 65670, 131206, 196742\n135=CA0015                 ; 135, 65671, 131207, 196743\n136=CA0016                 ; 136, 65672, 131208, 196744\n137=CA0017                 ; 137, 65673, 131209, 196745\n138=CA0018                 ; 138, 65674, 131210, 196746\n139=CA0019                 ; 139, 65675, 131211, 196747\n140=CA0020                 ; 140, 65676, 131212, 196748\n141=CA0021                 ; 141, 65677, 131213, 196749\n142=CAARMR                 ; 142, 65678, 131214, 196750\n143=GACSAM                 ; 143, 65679, 131215, 196751\n144=GATICK                 ; 144, 65680, 131216, 196752\n145=CAPYR01                ; 145, 65681, 131217, 196753\n146=CAPYR02                ; 146, 65682, 131218, 196754\n147=CAPYR03                ; 147, 65683, 131219, 196755\n148=CACRSH01               ; 148, 65684, 131220, 196756\n149=CACRSH02               ; 149, 65685, 131221, 196757\n150=CACRSH03               ; 150, 65686, 131222, 196758\n151=CACRSH04               ; 151, 65687, 131223, 196759\n152=CACRSH05               ; 152, 65688, 131224, 196760\n153=CAARAY                 ; 153, 65689, 131225, 196761\n154=GAICBM                 ; 154, 65690, 131226, 196762\n155=GAOLDCC1               ; 155, 65691, 131227, 196763\n156=GAOLDCC2               ; 156, 65692, 131228, 196764\n157=GAOLDCC3               ; 157, 65693, 131229, 196765\n158=GAOLDCC4               ; 158, 65694, 131230, 196766\n159=GAOLDCC5               ; 159, 65695, 131231, 196767\n160=GAOLDCC6               ; 160, 65696, 131232, 196768\n161=GAARTY                 ; 161, 65697, 131233, 196769\n162=TSTLAMP                ; 162, 65698, 131234, 196770\n163=NAHPAD                 ; 163, 65699, 131235, 196771\n164=GAKODK                 ; 164, 65700, 131236, 196772\n165=NAMNTK                 ; 165, 65701, 131237, 196773\n166=UFO                    ; 166, 65702, 131238, 196774\n167=AMMOCRAT               ; 167, 65703, 131239, 196775\n168=GAPAVE                 ; 168, 65704, 131240, 196776\n169=GAGREEN                ; 169, 65705, 131241, 196777\n170=INORNGLAMP             ; 170, 65706, 131242, 196778\n171=GAPLUG4                ; 171, 65707, 131243, 196779\n172=DJUGG                  ; 172, 65708, 131244, 196780\n173=DLIMPET                ; 173, 65709, 131245, 196781\n174=C_KODIAK               ; 174, 65710, 131246, 196782\n175=DGWEAP                 ; 175, 65711, 131247, 196783\n176=DNWEAP                 ; 176, 65712, 131248, 196784\n177=MSTL                   ; 177, 65713, 131249, 196785\n178=DDEFD                  ; 178, 65714, 131250, 196786\n179=AAOB                   ; 179, 65715, 131251, 196787\n180=CORE                   ; 180, 65716, 131252, 196788\n181=CROB                   ; 181, 65717, 131253, 196789\n182=KODIAK                 ; 182, 65718, 131254, 196790\n\n; advanced tech center\n[GATECH]\nName=GDI Tech Center\nPrerequisite=GAWEAP,GARADR\nStrength=500\nArmor=wood\nTechLevel=6\nAdjacent=2\nSight=6\nOwner=GDI\nCost=1500\nPoints=85\nPower=-200\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=5\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=1500, 1055, 815\nAIBuildThis=yes\nTogglePower=no\n\n; GDI weapons factory\n[GAWEAP]\nName=GDI War Factory\nImage=GAWEAP\nWeaponsFactory=yes\nPrerequisite=PROC,GAPILE\nFactory=UnitType\nDeployTime=.044\nStrength=1000\nArmor=heavy\nTechLevel=2\nSight=4\nAdjacent=2\nOwner=GDI\nCost=2000\nPoints=80\nPower=-30\nCapturable=true\nCrewed=yes\nBib=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=408, 880, 435\nAIBuildThis=yes\n\n; NOD weapons factory\n[NAWEAP]\nName=Nod War Factory\nWeaponsFactory=yes\nPrerequisite=PROC,NAHAND\nFactory=UnitType\nDeployTime=.044\nStrength=1000\nAdjacent=2\nArmor=heavy\nTechLevel=2\nSight=4\nOwner=Nod\nCost=2000\nPoints=80\nPower=-30\nCapturable=true\nCrewed=yes\nBib=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nNaturalSmokeLocation=-12,0,370\nMaxDebris=8\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=395, 750, 410\nAIBuildThis=yes\n\n; construction yard\n[GACNST]\nName=Construction Yard\nConstructionYard=yes\nStrength=1000\nArmor=heavy\nTechLevel=-1\nAdjacent=2\nFactory=BuildingType\nUndeploysInto=MCV\nSight=6\nOwner=GDI,Nod\nCost=2500\nPoints=80\nPower=0\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=10\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=1470, 1060, 1078\nAIBuildThis=yes\nTogglePower=no\n\n[GADPSA]\nName=Deployed Sensor Array\nTechLevel=-1\nStrength=600\nPoints=50\nCost=950\nSight=8\nPower=0\nArmor=wood\nSensorArray=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nVoiceSelect=25-I000\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nMaxDebris=2\nUndeploysInto=LPST\nBaseNormal=no\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=500, 500, 400\nHasRadialIndicator=true\nRadialColor=0,200,0\nCloakRadiusInCells=25\nTogglePower=no\nOwner=GDI,Nod\nCrewed=yes\n\n[GAICBM]\nName=Deployed ICBM\nTechLevel=-1\nStrength=400\nPoints=50\nPower=0\nArmor=wood\nICBMLauncher=yes\t;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.\nSensorArray=yes\t;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.\n;SuperWeapon=MultiSpecial\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nUndeploysInto=ICBM\nBaseNormal=no\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=500, 500, 400\nTogglePower=no\nOwner=Nod\nCrewed=yes\n\n[GATICK]\nName=Deployed Tick Tank\nTechLevel=-1\t;was 5\nStrength=350\nPoints=50\nCost=800\nPower=0\nArmor=concrete\nSight=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nUndeploysInto=TTNK\nBaseNormal=no\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nCrewed=yes\nPrimary=90mm\nElite=120mmx\nTurret=yes\nROT=5\nTickTank=yes\nTurretAnim=TTNKTUR\nTurretAnimIsVoxel=true\nTurretAnimX=4\nTurretAnimY=10\nTurretAnimZAdjust=-20\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys\nDamageSmokeOffset=500, 500, 400\nTrainable=yes\nTogglePower=no\nEliteAbilities=SENSORS\nHasStupidGuardMode=false\nOwner=Nod\n\n[GAARTY]\nName=Deployed Artillery\nTechLevel=-1\t;was 8\nStrength=300\nPoints=50\nCost=975\nPower=0\nArmor=light\nSight=9\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nUndeploysInto=ART2\nBaseNormal=no\nVoiceSelect=15-I004,15-I006,15-I008,15-I010,15-I012,15-I038,15-I040,15-I048\nVoiceMove=15-I014,15-I016,15-I018,15-I020,15-I022,15-I024,15-I060\nVoiceAttack=15-I026,15-I032,15-I044,15-I046,15-I050\nCrewed=yes\nPrimary=155mm\nTurret=yes\nROT=5\nArtillary=yes\nTurretAnim=ART2TUR\nTurretAnimIsVoxel=true\nTurretAnimX=-8\nTurretAnimY=15\nTurretAnimZAdjust=-20\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=500, 500, 400\nEliteAbilities=SELF_HEAL\nTrainable=yes\nTogglePower=no\nHasStupidGuardMode=false\nOwner=Nod\n\n; Deployed Juggernaut\n[DJUGG]\nName=Deployed Juggernaut\nImage=DJUGG\nTechLevel=-1\nStrength=400  ;was 350\nPoints=50\nCost=975\nPower=0\nArmor=light\nSight=9\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nUndeploysInto=JUGG\nBaseNormal=no\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nCrewed=yes\nPrimary=Jugg90mm\nROT=5\nIsJuggernaut=yes\nTurret=yes\nTurretAnim=DJUGG_A\nTurretAnimIsVoxel=false\nTurretAnimX=0\nTurretAnimY=0\nTurretAnimZAdjust=-30\nBarrelAnimIsVoxel=true\nVoxelBarrelFile=DJUGGBAR\nVoxelBarrelOffsetToPitchPivotPoint=15,0,-8\nVoxelBarrelOffsetToRotatePivotPoint=2,0,0\nVoxelBarrelOffsetToBuildingPivotPoint=4,2,3\nVoxelBarrelOffsetToBarrelEnd=350,75,0\nVoxelBarrelScale=.75\nStartFacing=4 ; DIR_S = 4 << 5\nStartPitch=2 ; DIR_E = 2 << 5\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=0,0,125\nEliteAbilities=SELF_HEAL\nTrainable=yes\nTogglePower=no\nHasStupidGuardMode=false\nOwner=GDI\n\n; Deployed Limpet Drone\n[DLIMPET]\nName=Limpet Mine\nStrength=100\nPoints=50\nCost=700\nSight=5\nPower=0\nArmor=none\nCloakable=yes\nCloakingSpeed=10\nPrimary=LIMP\nTechLevel=-1\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nBaseNormal=no\nUndeploysInto=LIMPET\nUnsellable=true\nIsLimpetMine=true\nOwner=GDI,Nod\n\n; Mobile weapons factory (GDI)\n[DGWEAP]\nName=Mobile War Factory\nImage=MWAR\nWeaponsFactory=yes\nFactory=UnitType\nDeployTime=.044\nStrength=800\nArmor=heavy\nTechLevel=-1\nBuildLimit=1\nSight=4\nBaseNormal=no\nCost=2000\nPoints=80\nPower=0\nCapturable=true\nCrewed=yes\nBib=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=408, 880, 435\nAIBuildThis=no\nUndeploysInto=MOBWARG\nIsMobileWar=yes\nOwner=GDI\n\n; Mobile weapons factory (Nod)\n[DNWEAP]\nName=Fist of Nod\nImage=MWAR\nWeaponsFactory=yes\nFactory=UnitType\nDeployTime=.044\nStrength=800\nArmor=heavy\nTechLevel=-1\nBuildLimit=1\nSight=4\nBaseNormal=no\nCost=2000\nPoints=80\nPower=0\nCapturable=true\nCrewed=yes\nBib=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=408, 880, 435\nAIBuildThis=no\nUndeploysInto=MOBWARN\nIsMobileWar=yes\nVoiceSelect=25-I000,25-I002,25-I004,25-I006\nVoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022\nVoiceAttack=25-I014,25-I022,25-I024,25-I026\nOwner=Nod\n\n; Deployed Mobile Stealth Generator\n[MSTL]\nName=Mobile Stealth Generator\nImage=MSTL\nCloakGenerator=yes\nCloakRadiusInCells=6\nHasRadialIndicator=true\nRadialColor=255,0,0\nStrength=200  ;was 600\nArmor=wood\nTechLevel=-1\nAdjacent=2\nSight=6\nCost=1600 ; w2000\nPoints=60\nPower=0\nPowered=false\nCapturable=false\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=5\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=0,0,100\nAIBuildThis=no ;\nUndeploysInto=SGEN\nIsMobileStealth=yes\nOwner=Nod\nCrewed=no\nBaseNormal=no\n\n; Tiberium Refinery\n[PROC]\nName=Tiberium Refinery\n;Image=NAREFN\t;Crim: Rules image ignores (pre)production anim Z, so we'll use image in art [PROC] instead\nRefinery=yes\nBib=yes\nPrerequisite=POWER\nStrength=900\nAdjacent=2\nArmor=heavy\nTechLevel=1\nFreeUnit=HARV\nDockUnload=yes\nSight=6\nOwner=GDI,Nod\nCost=2000\nPoints=80\nPower=-30\nStorage=80\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nHalfDamageSmokeLocation1=0,0,0\nMaxDebris=8\nPipScale=Tiberium\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=410, 100, 165\nAIBuildThis=yes\nTogglePower=no\n\n; storage silo\n[GASILO]\nName=Tiberium Silo\nPrerequisite=PROC\nStrength=300\nArmor=wood\nTechLevel=1\nAdjacent=2\nSight=2\nOwner=GDI,Nod\nCost=150\nPoints=25\nPower=-10\nStorage=60\nExplodes=yes\nCapturable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=2\nPipScale=Tiberium\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nDamageSmokeOffset=700, 700, 500\nTogglePower=no\n\n; helipad\n[GAHPAD]\nName=Helipad\nPrerequisite=GARADR\nStrength=600\nArmor=wood\nAdjacent=2\nTechLevel=5\nSight=5\nUnitReload=yes\nHelipad=yes\nOwner=GDI\nCost=500\nPoints=70\nPower=-10\nFactory=AircraftType\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=75, 270, 140\nAIBuildThis=yes\nHasStupidGuardMode=false\n\n; helipad\n[NAHPAD]\nName=Helipad\nPrerequisite=NARADR\nStrength=600\nArmor=wood\nAdjacent=2\nTechLevel=5\nSight=5\nUnitReload=yes\nHelipad=yes\nOwner=Nod\nCost=500\nPoints=70\nPower=-10\nFactory=AircraftType\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=275, 60, 140\nAIBuildThis=yes\nHasStupidGuardMode=false\n\n; GDI Radar\n[GARADR]\nName=Radar\nPrerequisite=PROC\nStrength=1000\nRadar=yes\nArmor=wood\nTechLevel=3\nAdjacent=2\nSight=10\nOwner=GDI\nCost=1000\nPoints=60\nPower=-40\nPowered=true\nCapturable=true\nSensors=yes\nCrewed=yes\nUpgrades=0\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=6\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=440, 200, 200\nAIBuildThis=yes\n\n; GDI Communications tower\n[GAPLUG]\nName=GDI Upgrade Center\nPrerequisite=PROC,GATECH\nStrength=1000\nRadar=no\nArmor=wood\nTechLevel=10\nAdjacent=2\nSight=6\nOwner=GDI\nCost=1000\nPoints=60\nPower=-150\nPowered=true\nCapturable=true\nSensors=yes\nCrewed=yes\nUpgrades=2\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=6\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=800, 550, 400\nAIBuildThis=yes\nSpecialThreatValue=1\nIsPlug=true\n\n; NOD Stealth Generator\n[NASTLH]\nName=Stealth Generator\nPrerequisite=PROC,NATECH\nCloakGenerator=yes\nCloakRadiusInCells=12\nHasRadialIndicator=true\nRadialColor=255,0,0\nStrength=600\nArmor=wood\nTechLevel=9\nAdjacent=2\nSight=6\nOwner=Nod\nCost=2500 ; w2000\nPoints=60\nPower=-350\nPowered=true\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=5\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=450, 200, 150\nAIBuildThis=yes ; commented out so that it's easier to debug base building\n\n; gdi power plant\n[GAPOWR]\nName=GDI Power Plant\nStrength=750\nArmor=wood\nTechLevel=1\nAdjacent=2\nSight=4\nOwner=GDI\nCost=300\nPoints=40\nPower=100\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nUpgrades=2\nMaxDebris=6\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=300, 300, 450\nTogglePower=no\n\n; nod power plant\n[NAPOWR]\nName=NOD Power Plant\nStrength=750\nArmor=wood\nTechLevel=1\nSight=4\nAdjacent=2\nOwner=Nod\nCost=300\nPoints=40\nPower=100\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=6\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=450, 200, 300\nTogglePower=no\n\n; Nod advanced power plant\n[NAAPWR]\nName=Advanced Power Plant\nPrerequisite=NAWEAP\nStrength=750\nArmor=wood\nTechLevel=7\nAdjacent=2\nSight=4\nOwner=Nod\nCost=500\nPoints=40\nPower=200\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=290, 570, 320\nTogglePower=no\n\n; NOD Tech Center\n[NATECH]\nName=NOD Tech Center\nPrerequisite=NAWEAP,NARADR\nStrength=500\nArmor=wood\nTechLevel=6\nAdjacent=2\nSight=6\nOwner=Nod\nCost=1500\nPoints=85\nPower=-100\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=5\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=200, 325, 200\nAIBuildThis=yes\nTogglePower=no\n\n; NOD Barracks\n[NAHAND]\nName=Hand Of Nod\nPrerequisite=POWER\nStrength=800\nArmor=wood\nTechLevel=1\nAdjacent=2\nSight=5\nOwner=Nod\nCost=300\nPoints=30\nPower=-20\nFactory=InfantryType\nCrewed=yes\nCapturable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nExitCoord = 0,0,0\nNODBarracks=yes\nDamageParticleSystems=SparkSys,SmallGreySmokeSys,BigGreySmokeSys\nDamageSmokeOffset=480, 96, 125\nAIBuildThis=yes\n\n; GDI Barracks\n[GAPILE]\nName=Barracks\nPrerequisite=POWER\nStrength=800\nArmor=wood\nFactory=InfantryType\nAdjacent=2\nTechLevel=1\nSight=5\nOwner=GDI\nCost=300\nPoints=30\nPower=-20\nCrewed=yes\nCapturable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nExitCoord = -64,64,0\nGDIBarracks=yes\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=215, 395, 200\nAIBuildThis=yes\n\n; service depot\n[GADEPT]\nName=Service Depot\nPrerequisite=FACTORY\nStrength=1100\nArmor=wood\nTechLevel=7\nAdjacent=2\nSight=5\nUnitRepair=yes\nUnitReload=yes\nOwner=GDI ; removed from Nod side\nCost=1200\nPoints=80\nPower=-30\nCapturable=true\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=270, 580, 260\nAIBuildThis=yes\nHasStupidGuardMode=false\n\n; Pavenent\n[GAPAVE]\nName=Pavement\nStrength=150\nPrerequisite=BARRACKS\nHigh=yes\nArmor=concrete\nTechLevel=6\nAdjacent=3\nSight=0\nSelectable=no\nInsignificant=yes\nNominal=yes\nOwner=GDI,Nod\nCost=75\nBaseNormal=no\nPoints=5\nRepairable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\nToTile=pvclr01\n\n; green lat (not used)\n[GAGREEN]\nName=Green Building\nImage=null\nStrength=150\nPrerequisite=GAPILE\nHigh=yes\nArmor=concrete\nTechLevel=-1\nAdjacent=3\nSight=0\nSelectable=no\nInsignificant=yes\nNominal=yes\nOwner=GDI,Nod\nCost=100\nBaseNormal=no\nPoints=5\nRepairable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\nToTile=Green01\n\n; sandbag wall\n[GASAND]\nName=Sandbags\nStrength=250\nPrerequisite=BARRACKS\nArmor=light\nCrushSound=SANDBAG1\nCrushable=yes\nWall=yes\nTechLevel=-1\nAdjacent=4\nSight=0\nNominal=yes\nSelectable=no\nOwner=GDI,NOD\nCost=125\t;25\nBaseNormal=no\nInsignificant=yes\nPoints=1\nRepairable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\n;IsBase=no\t;Crim: not a rules flag. Use BaseNormal.\n\n; concrete wall\n[GAWALL]\nName=Concrete Wall\nStrength=150\nPrerequisite=GAPILE\nHigh=yes\nArmor=concrete\nTechLevel=6\nAdjacent=4\nWall=yes\nSight=1\nSelectable=no\nInsignificant=yes\nNominal=yes\nOwner=GDI\nCost=250\t;50\nBaseNormal=no\nPoints=5\nRepairable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\n;IsBase=no\nGuardRange=5\n\n; NOD wall\n[NAWALL]\nName=Nod Wall\nStrength=150\nPrerequisite=NAHAND\nHigh=yes\nArmor=concrete\nTechLevel=6\nAdjacent=4\nWall=yes\nSight=1\nSelectable=no\nInsignificant=yes\nNominal=yes\nOwner=Nod\nCost=250\t;50\nBaseNormal=no\nPoints=5\nRepairable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\n;IsBase=no\nGuardRange=5\n\n; Bridge repair hut\n[CABHUT]\nName=Bridge repair hut\nStrength=2000\nImmune=yes\nLegalTarget=no\nNominal=yes\nTechLevel=-1\nRadarInvisible=yes\nRepairable=true\nSelectable=no\nInsignificant=yes\nBridgeRepairHut=yes\nAdjacent=0\nBaseNormal=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=0\t; This value MUST be 0 for all building addons\n\n; Nod pulse cannon\n[NAPULS]\nName=EMP Cannon\nStrength=500\nArmor=heavy\nPrerequisite=Radar\nTechLevel=6\nSight=8\nAdjacent=2\nOwner=Nod,GDI\nCost=1000\nTurret=yes\nPoints=50\nPower=-150\nSensors=yes\nCrewed=yes\nROT=12\nEMPulseCannon=yes\nSuperWeapon=EMPulseSpecial\nPrimary=EMPulseWeapon\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=5\nTurretAnim=PULSCAN\nTurretAnimIsVoxel=true\nTurretAnimY=7\nTurretAnimX=1\nTurretAnimZAdjust=-100\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=350, 125, 100\nHasStupidGuardMode=false\n\n; GDI Component Tower\n[GACTWR]\nName=Component Tower\nStrength=500\nArmor=light\nPrerequisite=GAPILE\nTechLevel=2\nSight=4\nAdjacent=3\nOwner=GDI\nCost=200\nTurret=yes\nPoints=50\nPower=-10\nSensors=yes\nBaseNormal=no\nCrewed=no\nROT=12\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nTurretAnimZAdjust=-45   ;WST 6/18/99 was 50 but cause z buffer problem: Component towers turret animation depends on what the player attaches :\nHasSpotlight=false;\nMaxDebris=2\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nDamageSmokeOffset=500, 500, 400\n;IsBase=no\t;Crim: not a rules flag\nHasStupidGuardMode=false\n\n; GDI gate in wall\n[GAGATE_A]\nName=Gate\nStrength=350\nPrerequisite=GAPILE\nArmor=heavy\nTechLevel=6\nSelectable=yes\nCapturable=false\nInsignificant=yes\nAdjacent=4\nOwner=GDI\nCost=250\nBaseNormal=no\nPoints=50\nRepairable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nGate=yes\nDeployTime=.044\nGateCloseDelay=.2\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n;IsBase=no\n\n; GDI gate in wall\n[GAGATE_B]\nName=Gate\nStrength=350\nArmor=heavy\nPrerequisite=GAPILE\nTechLevel=6\nAdjacent=4\nSelectable=yes\nCapturable=false\nInsignificant=yes\nOwner=GDI\nCost=250\nBaseNormal=no\nPoints=50\nRepairable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nGate=yes\nDeployTime=.044\nGateCloseDelay=.2\nMaxDebris=2\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n;IsBase=no\n\n; Nod gate in wall\n[NAGATE_A]\nName=Gate\nStrength=350\nArmor=heavy\nPrerequisite=NAHAND\nTechLevel=6\nAdjacent=4\nSelectable=yes\nCapturable=false\nInsignificant=yes\nOwner=Nod\nCost=250\nBaseNormal=no\nPoints=50\nRepairable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nGate=yes\nDeployTime=.044\nGateCloseDelay=.2\nMaxDebris=2\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n;IsBase=no\n\n; Nod gate in wall\n[NAGATE_B]\nName=Gate\nStrength=350\nArmor=heavy\nPrerequisite=NAHAND\nTechLevel=6\nAdjacent=4\nSelectable=yes\nCapturable=false\nInsignificant=yes\nOwner=Nod\nCost=250\nBaseNormal=no\nPoints=50\nRepairable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nGate=yes\nDeployTime=.044\nGateCloseDelay=.2\nMaxDebris=2\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n;IsBase=no\n\n; Light post\n[TSTLAMP]\nName=AlphaLightPost\nImage=GALITE\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=no\t\t; why would we want guys to come out of the lightpost? BNA 7/15/99\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys\nInsignificant=yes\nAlphaImage=ALPHATST\n\n; Light post\n[GALITE]\nName=Light Post\nImage=GALITE\nStrength=600\nArmor=wood\nOwner=GDI,NOD\nCost=200\nTechLevel=-1 ; changed from 12\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=NO\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=5000\nLightIntensity=0.2\nLightRedTint=0.05\nLightGreenTint=0.05\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAlphaImage=NONE\nPowered=true\n\n; Black Light post\n[NEGLAMP]\nName=Negative Light Post\nImage=GALITE\nStrength=1000\nArmor=wood\nTechLevel=-1\nInvisibleInGame=yes\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nLightVisibility=3500\nLightIntensity=-0.15\nLightRedTint=0.03\nLightGreenTint=0.04\nLightBlueTint=0.04\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nInsignificant=yes\n\n; Light post\n[INGALITE]\nName=Invisible Light Post\nImage=GALITE\nInvisibleInGame=yes\nInsignificant=yes\nSelectable=no\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=5000\nLightIntensity=0.2\nLightRedTint=0.05\nLightGreenTint=0.05\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Light post\n[REDLAMP]\nName=Red Light Post\nImage=GALITE\nInsignificant=yes\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=1.5\nLightGreenTint=0.01\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAlphaImage=NONE\nPowered=true\n\n; Light post\n[NEGRED]\nName=Negative Red Light\nImage=GALITE\nInsignificant=yes\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=-0.05\nLightRedTint=-1.5\nLightGreenTint=0.01\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n\n; Light post\n[GRENLAMP]\nName=Green Light Post\nImage=GALITE\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=1.5\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAlphaImage=NONE\nPowered=true\nInsignificant=yes\n\n; Light post\n[BLUELAMP]\nName=Blue Light Post\nImage=GALITE\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=0.01\nLightBlueTint=0.7\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAlphaImage=NONE\nPowered=true\nInsignificant=yes\n\n; Light post\n[YELWLAMP]\nName=Yellow Light Post\nImage=GALITE\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=.01\nLightRedTint=1.5\nLightGreenTint=1.5\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\nInsignificant=yes\n\n; Light post\n[INYELWLAMP]\nName=Invisible Yellow Light Post\nImage=GALITE\nInsignificant=yes\nSelectable=no\nInvisibleInGame=yes\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=4000\nLightIntensity=.01\nLightRedTint=1.5\nLightGreenTint=1.5\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Light post\n[PURPLAMP]\nName=Purple Light Post\nImage=GALITE\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=3000\nLightIntensity=0.01\nLightRedTint=2.0\nLightGreenTint=0.01\nLightBlueTint=2.0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAlphaImage=NONE\nPowered=true\nInsignificant=yes\n\n; Light post\n[INPURPLAMP]\nName=Invisible Purple Light Post\nImage=GALITE\nSelectable=no\nInsignificant=yes\nInvisibleInGame=yes\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLightVisibility=3000\nLightIntensity=0.01\nLightRedTint=2.0\nLightGreenTint=0.01\nLightBlueTint=2.0\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Light post\n[INORANLAMP]\nName=Orange Light Post\nImage=GALITE\nSelectable=no\nInvisibleInGame=yes\nStrength=600\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLightVisibility=3000\nLightIntensity=0.01\nLightRedTint=2.0\nLightGreenTint=1.4\nLightBlueTint=0.3\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\nInsignificant=yes\n\n[INORNGLAMP]\nName=Invisible Orange Light Post\nImage=GALITE\nInvisibleInGame=yes\nInsignificant=yes\nSelectable=no\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=1\nLightVisibility=3000\nLightIntensity=0.01\nLightRedTint=1.1\nLightGreenTint=0.55\nLightBlueTint=0.01\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Light post\n[INGRNLMP]\nName=Invisible Green Light Post\nImage=GALITE\nSelectable=no\nInvisibleInGame=yes\nInsignificant=yes\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=1.5\nLightBlueTint=0.01\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Light post\n[INREDLMP]\nName=Invisible Red Light Post\nImage=GALITE\nSelectable=no\nInsignificant=yes\nInvisibleInGame=yes\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=1.5\nLightGreenTint=0.01\nLightBlueTint=0.01\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Invisible Light post\n[INBLULMP]\nName=Invisible Blue Light Post\nSelectable=no\nInvisibleInGame=yes\nInsignificant=yes\nImage=GALITE\nStrength=6000\nArmor=wood\nTechLevel=-1\nNominal=yes\nSight=0\nPoints=30\nPower=0\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=0.01\nLightBlueTint=0.7\nDamageParticleSystems=SparkSys,LGSparkSys\nPowered=true\n\n; Temple of NOD\n[NATMPL]\nName=Temple of NOD\nPrerequisite=NATECH\nStrength=1000\nArmor=wood\nTechLevel=10\nAdjacent=3\nSight=6\nOwner=Nod\nCost=2000\nPoints=60\nPower=-200\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nThreatPosed=0\t; This value MUST be 0 for all building addons\nSuperWeapon=HuntSeekSpecial\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=1210, 680, 400\nAIBuildThis=yes\nIsTemple=yes\n\n; pyramid of NOD\n[NTPYRA]\nName=NOD Pyramid\nStrength=1500\nArmor=heavy\nTechLevel=-1\nAdjacent=2\nSight=0\nOwner=Nod\nCost=1000\nPoints=60\nPower=-40\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=1150, 660, 475\n\n;Kodiak\n[GAKODK]\nName=GDI Kodiak\nStrength=1500\nArmor=heavy\nTechLevel=-1\nAdjacent=2\nSight=10\nOwner=GDI\nCost=1000\nPoints=60\nPower=0\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=1200, 880, 635\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\nTogglePower=no\n\n;Large Kodiak by Lin Kuei Ominae\n[KODIAK]\nName=GDI Kodiak\nStrength=1750\nArmor=heavy\nTechLevel=-1\nAdjacent=2\nSight=10\nOwner=GDI\nCost=1000\nPoints=60\nPower=0\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=2400, 1960, 1535\n;IsBase=no\nBaseNormal=no\nTogglePower=no\n\n;Montauk\n[NAMNTK]\nName=NOD Montauk\nStrength=1500\nArmor=heavy\nTechLevel=-1\nAdjacent=2\nSight=0\nOwner=NOD\nCost=1000\nPoints=60\nPower=0\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=700, 1180, 800\n;IsBase=no\nBaseNormal=no\nTogglePower=no\n\n; Dropship bay (obsolete)\n;[GADROP]\n;Name=Dropship Bay\n;Prerequisite=DOME,GATECH\n;Strength=3000\n;TechLevel=9\n;Adjacent=2\n;Sight=10\n;Owner=GDI\n;Cost=1000\n;Points=60\n;Power=-40\n;Powered=true\n;Capturable=true\n;Sensors=yes\n;Crewed=yes\n;Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\n;MaxDebris=8\n;ThreatPosed=0\t; This value MUST be 0 for all building addons\n;DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\n\n;NOD Radar Facility\n[NARADR]\nName=NOD Radar\nPrerequisite=PROC\nStrength=1000\nRadar=yes\nArmor=wood\nTechLevel=3\nAdjacent=2\nSight=10\nOwner=NOD\nCost=1000\nPoints=60\nPower=-40\nPowered=true\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=220, 390, 150\nAIBuildThis=yes\n\n; Nod tiberium waste facility\n[NAWAST]\nName=Tiberium Waste Facility\nPrerequisite=NAMISL\nStrength=400\nArmor=wood\nTechLevel=10\nAdjacent=2\nSight=5\nOwner=Nod\nCost=1600\nPoints=60\nPower=-40\nFreeUnit=WEED\nCapturable=true\nSensors=yes\nCrewed=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=400, 625, 200\nWeeder=yes\nBib=yes\nPipScale=Tiberium\n;SuperWeapon=ChemicalSpecial\nAIBuildThis=yes\nTogglePower=no\nBuildLimit=1\n\n; NOD Obelisk\n[NAOBEL]\nName=Obelisk of Light\nPrerequisite=NATECH\nStrength=725\nArmor=wood\nTechLevel=9\nAdjacent=2\nSight=8\nOwner=Nod\nCost=1500\nPoints=30\nPower=-150\nCrewed=yes\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPrimary=LaserFire\nTurret=no\nTurretAnim=NAOBEL_B\nTurretAnimZAdjust=-100\nMaxDebris=4\nThreatPosed=40\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=355, 525, 225\nIsBaseDefense=yes\nBaseNormal=no\nPowered=yes\nHasStupidGuardMode=false\n\n; Nod missile silo\n[NAMISL]\nName=Missile Silo\nSuperWeapon=MultiSpecial\nSuperWeapon2=ChemicalSpecial\nPrerequisite=NATECH\nStrength=1000\nArmor=wood\nTechLevel=10\nAdjacent=2\nSight=4\nOwner=Nod\nCost=1300\nPoints=30\nPower=-50\nCrewed=yes\nCapturable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=6\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nAIBuildThis=yes\nSpecialThreatValue=1\nNukeSilo=yes\nHasStupidGuardMode=false\n\n; Vulcan cannon add-on for component tower\n[GAVULC]\nName=Vulcan Cannon\nImage=GAVULC\nPrerequisite=GACTWR,GAPILE\nTechLevel=2\nArmor=wood\nSight=7\nOwner=GDI\nCost=150\nPoints=30\nPower=-20\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gactwr\nPowersUpToLevel=1\nPrimary=VulcanTower\nSecondary=VulcanTower\nTurret=yes\nThreatPosed=0\t; This value MUST be 0 for all building addons\nIsBaseDefense=yes\n\n; Rocket launcher addon for component tower\n[GAROCK]\nName=RPG Upgrade\nImage=GAROCK\nPrerequisite=GACTWR,GAPILE\nTechLevel=9\nArmor=wood\nSight=8\nOwner=GDI\nCost=600\nPoints=30\nPower=-20\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gactwr\nPowersUpToLevel=2\nPrimary=RPGTower\nTurret=yes\nThreatPosed=0\t; This value MUST be 0 for all building addons\nIsBaseDefense=yes\n\n; SAM addon for component tower\n[GACSAM]\nName=SAM Upgrade\nImage=GACSAM\nPrerequisite=GACTWR,GARADR\nTechLevel=5\nArmor=wood\nSight=10\nOwner=GDI\nCost=300\nPoints=30\nPower=-30\nCrewed=no\nCapturable=no\nExplosion=TWLT070\nPowersUpBuilding=gactwr\nPowersUpToLevel=3\nPrimary=RedEye2\nSecondary=RedEye2\nTurret=yes\nThreatPosed=0\t; This value MUST be 0 for all building addons\nIsBaseDefense=yes\nPowered=yes\n\n; GDI Power plant upgrade.\n[GAPOWRUP]\nName=Power Turbine\nPrerequisite=GAPOWR\nImage=GAPOWR_B\nTechLevel=7\nArmor=wood\nSight=1\nOwner=GDI\nCost=100\nPoints=30\nPower=50\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gapowr\nPowersUpToLevel=-1\nThreatPosed=0\t; This value MUST be 0 for all building addons\n\n; OBSOLETE ; Upgrade No. 1 for Upgrade Center\n[GAPLUG1]\nName=Threat Rating Node\nImage=GAPLUG_D\nPrerequisite=GAPLUG\nTechLevel=-1\nArmor=wood\nSight=1\nOwner=GDI\nCost=500\nPoints=30\nPower=-20\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gaplug\nPowersUpToLevel=-1\nThreatPosed=0\t; This value MUST be 0 for all building addons\nIsThreatRatingNode=true\n\n; Upgrade No. 2 for Upgrade Center\n[GAPLUG2]\nName=Seeker Control\nImage=GAPLUG_E\nPrerequisite=GAPLUG,GATECH,GAWEAP\nTechLevel=10\nArmor=wood\nSight=1\nOwner=GDI\nCost=1000\nPoints=30\nPower=-50\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gaplug\nPowersUpToLevel=-1\nThreatPosed=0\t; This value MUST be 0 for all building addons\nSuperWeapon=HuntSeekSpecial\nAIBuildThis=yes\n\n; Upgrade No. 3 for Upgrade Center\n[GAPLUG3]\nName=Ion Cannon Uplink\nImage=GAPLUG_F\nPrerequisite=GAPLUG,GATECH\nTechLevel=10\nArmor=wood\nSight=1\nOwner=GDI\nCost=1500\nPoints=30\nPower=-100\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gaplug\nPowersUpToLevel=-1\nSuperWeapon=IonCannonSpecial\nThreatPosed=0\t; This value MUST be 0 for all building addons\nAIBuildThis=yes\n\n; Upgrade No. 4 for Upgrade Center\n[GAPLUG4]\nName=Drop Pod Node\nImage=GAPLUG_D\nPrerequisite=GAPLUG\nTechLevel=11\nArmor=wood\nSight=1\nOwner=GDI\nCost=1000  ;was 1200\nPoints=30\nPower=-20\nCrewed=no\nCapturable=no\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPowersUpBuilding=gaplug\nPowersUpToLevel=-1\nThreatPosed=0\t; This value MUST be 0 for all building addons\nSuperWeapon=DropPodSpecial\nAIBuildThis=no\n\n; Firestorm defense\n[GAFIRE]\nName=Fire Storm Generator\nStrength=800\nArmor=heavy\nTechLevel=9\nPrerequisite=GATECH\nAdjacent=2\nSight=5\nOwner=GDI\nCost=2000\nPoints=30\nPower=-200\nCrewed=yes\nCapturable=true\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=9\nSuperWeapon=FirestormSpecial\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=410, 600, 290\n\n; Laser fence post\n[NAPOST]\nName=Laser Fence Post\nPrerequisite=NAAPWR\nStrength=300\nArmor=concrete\nTechLevel=8\nAdjacent=3\nSight=4\nOwner=Nod\nCost=200\nBaseNormal=no\nPoints=30\nPower=-25\nCrewed=no\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nLaserFencePost=yes\nMaxDebris=2\nPowered=yes\nThreatPosed=0\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nBaseNormal=no\nGuardRange=10  ; Used to set max. intra-post distance\n\n[NAFNCE]\nName=Laser Fence Section\nStrength=800\nArmor=concrete\nTechLevel=-1\nSight=1\nOwner=Nod\nCapturable=false\nCost=0\nPoints=00\nPower=0\nSelectable=no\nCrewed=no\nLaserFence=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=10\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nBaseNormal=no\nLegalTarget=false\nTogglePower=no\nRepairable=false\t;don't allow this to be blown up with C4\n\n; Laser turret\n[NALASR]\nName=Laser\nStrength=500\nArmor=wood\nPrerequisite=NAHAND\nTechLevel=2\nAdjacent=4\nROT=10\nSight=7\nOwner=Nod\nCost=300\nBaseNormal=no\nPoints=30\nPower=-40\nCrewed=no\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=2\nPrimary=LaserFire2\nTurret=yes\nTurretAnim=LASER\nTurretAnimIsVoxel=true\nTurretAnimX=-8\nTurretAnimY=16\nTurretAnimZAdjust=-40\nThreatPosed=30\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\nIsBaseDefense=yes\nHasStupidGuardMode=false\n\n; SAM\n[NASAM]\nName=Sam\nStrength=600\nArmor=wood\nTechLevel=5\nPrerequisite=NARADR\nAdjacent=4\nSight=10\nOwner=Nod\nCost=500\nBaseNormal=no\nPoints=30\nPower=-30\nCrewed=no\nPrimary=RedEye2\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nThreatPosed=0\t; This value should be 0 for objects that are purely anti-air, since aircraft do not use the threat values\nDamageParticleSystems=SparkSys,LGSparkSys\nIsBaseDefense=yes\nPowered=yes\nTurret=yes\nTurretAnim=NASAM_A\nTurretAnimIsVoxel=false\nTurretAnimX=-2\nTurretAnimY=10\nTurretAnimZAdjust=-20\nHasStupidGuardMode=false\n\n[GAFSDF]\nName=Firestorm Wall Section\nStrength=200\nArmor=concrete\nPrerequisite=GAFIRE\nTechLevel=9\nRepairable=false\nSight=2\nOwner=GDI\nCapturable=false\nCost=250\t;50\nPoints=00\nPower=-2\nSelectable=no\nCrewed=no\nFirestormWall=yes\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nThreatPosed=20\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,LGSparkSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\nTogglePower=no\nInsignificant=yes\nGuardRange=5\n\n[ABAN01]\nName=WS Logging Company\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN02]\nName=Pannullo Hacienda\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN03]\nName=Abandoned Factory\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN04]\nName=City Hall\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN05]\nName=Hunting Lodge\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN06]\nName=Local Inn & Lodging\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN07]\nName=Church\nTechLevel=-1\nStrength=350\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN08]\nName=Abandoned Warehouse\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN09]\nName=Tall's Residence\nTechLevel=-1\nStrength=350\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN10]\nName=Denzil's Last Chance Motel\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN11]\nName=Miele Manor\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN12]\nName=Kettler's Place\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN13]\nName=Long's Home\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN14]\nName=Local Store\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN15]\nName=Adam's House\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN16]\nName=Gas Station\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=2\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN17]\nName=Gas Pumps\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=2\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[ABAN18]\nName=Gas Station Sign\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=2\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0001]\nName=Rade's Roadhouse\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[CA0002]\nName=Sandberg and Son's\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0003]\nName=Temp Housing\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0004]\nName=Waystation\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0005]\nName=Ferbie's 4 Sale\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0006]\nName=Deluxe Accomodations\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0007]\nName=Field Generator\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0008]\nName=Subterranean Dwelling\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0009]\nName=Subterranean Dwelling\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0010]\nName=Leary Traveller Inn\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0011]\nName=Water Tank\nTechLevel=-1\nStrength=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0012]\nName=Greenhouse\nTechLevel=-1\nStrength=100\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0013]\nName=Water Purifier\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0014]\nName=Observation Tower\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0015]\nName=Port-A-Shack\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0016]\nName=Port-A-Shack Deluxe\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0017]\nName=Energy Transformer\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0018]\nName=Solar Panel\nTechLevel=-1\nStrength=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0019]\nName=Solar Panel\nTechLevel=-1\nStrength=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0020]\nName=Solar Panel\nTechLevel=-1\nStrength=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CA0021]\nName=Solar Panel\nTechLevel=-1\nStrength=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=light\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[GAOLDCC1]\nName=Old Construction Yard\nTechLevel=-1\nStrength=400\nInsignificant=yes\nCapturable=false\nRepairable=false\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[GAOLDCC2]\nName=Old Temple\nTechLevel=-1\nStrength=400\nInsignificant=yes\nCapturable=false\nRepairable=false\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[GAOLDCC3]\nName=Old Weapons Factory\nTechLevel=-1\nStrength=400\nCapturable=false\nRepairable=false\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[GAOLDCC4]\nName=Old Refinery\nTechLevel=-1\nStrength=400\nCapturable=false\nRepairable=false\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[GAOLDCC5]\nName=Old Advanced Power Plant\nTechLevel=-1\nStrength=400\nCapturable=false\nRepairable=false\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[GAOLDCC6]\nName=Old Silos\nTechLevel=-1\nStrength=400\nCapturable=false\nRepairable=false\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n; Use Ammo to specify the number of times to allow healing.\n[CAHOSP]\nName=Civilian Hospital\nTechLevel=-1\nStrength=800\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nHospital=yes\nPipScale=Ammo\nAmmo=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n; Use Ammo to specify number of time the building can be used to upgrade infantry\n[CAARMR]\nName=Civilian Armory\nTechLevel=-1\nStrength=800\nImmune=no\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nArmory=yes\nPipScale=Ammo\nAmmo=5\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CAPYR01]\nName=Pyramid\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\n;IsBase=no\nBaseNormal=no\n\n[CAPYR02]\nName=Pyramid\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\n;IsBase=no\nBaseNormal=no\n\n[CAPYR03]\nName=Pyramid\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\n;IsBase=no\nBaseNormal=no\n\n[CACRSH01]\nName=Crash 1\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nSelectable=no\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[CACRSH02]\nName=Crash 2\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[CACRSH03]\nName=Crash 3\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[CACRSH04]\nName=Crash 4\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[CACRSH05]\nName=Crash 5\nTechLevel=-1\nStrength=400\nImmune=yes\nLegalTarget=no\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[CAARAY]\nName=Civilian Array\nTechLevel=-1\nStrength=400\nLegalTarget=yes\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=3\n;IsBase=no\nBaseNormal=no\n\n[GASPOT]\nName=Light Tower\nTechLevel=-1\nStrength=400\nPoints=50\nPower=-10\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nHasSpotlight=true\nMaxDebris=2\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=450, 500, 710\n;IsBase=no\nBaseNormal=no\n\n[CITY01]\nName=Connelly Court Apts.\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[CITY02]\nName=Lightner's Luxury Suites\nTechLevel=-1\nStrength=700\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY03]\nName=Office Building\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY04]\nName=Westwood Stock Exchange\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY05]\nName=Daily Sun Times\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY06]\nName=YEO-CA Cola Corp.\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY07]\nName=Urban Housing\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY08]\nName=Yee's Discount Liquor\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY09]\nName=Abandoned Warehouse\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY10]\nName=Urban Storefront\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY11]\nName=Ambrose Lounge\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY12]\nName=Bostic Tower\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY13]\nName=Hewitt Hair Salon\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY14]\nName=Business Offices\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY15]\nName=2nd National Bank\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY16]\nName=Highrise Hotel\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY17]\nName=The Projects\nTechLevel=-1\nStrength=300\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY18]\nName=Archer Asylum\nTechLevel=-1\nStrength=600\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY19]\nName=Fill'er Up-Pump'N'Go\nTechLevel=-1\nStrength=500\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY20]\nName=Gas Pump\nTechLevel=-1\nStrength=250\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=wood\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=6\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY21]\nName=Gas Station Sign\nTechLevel=-1\nStrength=100\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=none\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CITY22]\nName=Church\nTechLevel=-1\nStrength=100\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=none\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\nBaseNormal=no\n\n[CTVEGA]\nName=Vega's Pyramid\nTechLevel=-1\nStrength=100\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=none\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nDamageParticleSystems=SmallGreySSys,BigGreySmokeSys\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[BBOARD01]\nName=Eat at Rade's Roadhouse\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD02]\nName=Drink YEO-CA Cola!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD03]\nName=Hamburgers $.99\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD04]\nName=Visit Scenic Las Vegas\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD05]\nName=Rooms $29 a nite\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD06]\nName=Kaspm's Tiberium Warhouse\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD07]\nName=Alkaline's Battery Superstore\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD08]\nName=Alex-gators petshop just ahead!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD09]\nName=TacticX games rock!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD10]\nName=WW Surf and Turf hits the spot!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD11]\nName=Only 11 miles to Zydeko's cafe!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD12]\nName=No escape from Archer's Asylum!\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD13]\nName=Stop in at Hewitt's hair salon\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD14]\nName=Billy Bob's Harvester school\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD15]\nName=Pannullo's hacienda es bueno\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[BBOARD16]\nName=Join GDI: We save lives.\nTechLevel=-1\nStrength=400\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=0\nSelectable=no\n;IsBase=no\nBaseNormal=no\n\n[CTDAM]\nName=Dam\nTechLevel=1\nStrength=1000\nPower=200\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nPlaceAnywhere=yes\n;DamageParticleSystems=SparkSys,LGSparkSys WST 7/16 commented out cuz it looked like bad palette\nDamageSmokeOffset=500, 100, 500\n;IsBase=no\t;Crim: not a rules flag\nBaseNormal=no\n\n[UFO]\nName=Scrin Ship\nTechLevel=-1\nStrength=1000\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=8\nPlaceAnywhere=yes\n;IsBase=no\nBaseNormal=no\n\n; Kodiak crash\n[C_KODIAK]\nName=Kodiak Crash\nTechLevel=-1\nStrength=1000\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=heavy\nMaxDebris=0\nPlaceAnywhere=yes\n\n[AMMOCRAT]\nName=Ammo Crates\nImage=AMMO01\nSelectable=no\nExplodes=yes\nTechLevel=-1\nStrength=1\nInsignificant=yes\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=none\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nMaxDebris=4\nPlaceAnywhere=yes\n;IsBase=no\nBaseNormal=no\nThreatPosed=10\nSpecialThreatValue=1\n\n; Core Defender (deployed)\n[DDEFD]\nName=Core Defender\nImage=DEFD\nCapturable=false\nOwner=Civilian\nUndeploysInto=DEFENDER\nMaxDebris=10\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nStrength=9999\nArmor=concrete\nTechLevel=-1\nAdjacent=2\nSight=6\nCost=10000\nPoints=10000\nPower=0\nPowered=false\nSensors=yes\nThreatPosed=0\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=375,550,500\nAIBuildThis=no\nIsCoreDefender=yes\nUndeploySound=COREUP1\nImmune=yes\n\n; Cabal Core\n[CORE]\nName=Cabal Core\nTechLevel=-1\nStrength=3000\nNominal=yes\nRadarInvisible=yes\nPoints=5\nArmor=concrete\nMaxDebris=0\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=-60,60,200\nPlaceAnywhere=yes\n\n; NOD AA Obelisk\n[AAOB]\nName=Obelisk of Darkness\nImage=OBL2\nPrerequisite=\nStrength=1000\nArmor=concrete\nTechLevel=-1 ;9\nAdjacent=2\nSight=8\nOwner=Civilian\nCost=1500\nPoints=30\nPower=0\nCrewed=yes\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPrimary=AALaserFire\nTurret=no\nTurretAnim=OBL2_C\nTurretAnimZAdjust=-3\nTurretChargeAnimRate=1\nTurretAnimIsExclusive=yes\nMaxDebris=4\nThreatPosed=40\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=-60,60,200\nIsBaseDefense=yes\nBaseNormal=no\nPowered=yes\nHasStupidGuardMode=false\n\n; CABAL Core Obelisk Ground\n[CROB]\nName=CABAL Obelisk\nImage=OBL1\nPrerequisite=\nStrength=1000\nArmor=concrete\nTechLevel=-1\nAdjacent=2\nSight=8\nOwner=Civilian\nCost=1500\nPoints=30\nPower=0\nCrewed=yes\nCapturable=false\nExplosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60\nPrimary=CABLaser\nTurret=no\nTurretAnim=OBL1_C\nTurretAnimZAdjust=-100\nTurretChargeAnimRate=1\nTurretAnimIsExclusive=yes\nMaxDebris=4\nThreatPosed=40\t; This value MUST be 0 for all building addons\nDamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys\nDamageSmokeOffset=120,0,30\nIsBaseDefense=yes\nBaseNormal=no\nPowered=yes\nHasStupidGuardMode=false\n\n; ******* Weapon Statistics *******\n; The weapons specified here are attached to the various combat\n; units and buildings.\n\n; Anim = animation to display as a firing effect [use 8 for directional variation]\n; Burst = number of rapid succession shots from this weapon (def=1)\n; Camera = Reveals area around firer (def=no)?\n; Charges = Does it have charge-up-before-firing logic (def=no)?\n; Damage = the amount of damage (unattenuated) dealt with every bullet\n; Floater = floats like a frizbee\n; Lobber = does the projectile fly to target in a high arc (def=no)?\n; Projectile = projectile characteristic to use\n; ROF = delay between shots [15 = 1 second at middle speed setting]\n; Range = maximum cell range\n; MinimumRange = minimum range to target (def=0)\n; Report = List of sounds to random play when firing\n; Speed = speed of projectile to target (100 is maximum)\n; Warhead = warhead to attach to projectile\n; Supress = Should nearby friendly buildings be scanned for and if found, discourage firing on target (def=no)?\n; TurboBoost = Should the weapon get a boosted speed bonus when firing upon aircraft?\n; UseFireParticles = Should the weapon spawn a flame particle system? (def = no)\n; Bright = Does this weapons bullet cause a lighting effect when it impacts (def=no)? If set, this will override the warheads 'Bright' flag.\n; IonSensitive = Shuts down during an ion storm (def=no)?\n\n[Weapons]\n00=Vulcan2\n01=MultiLauncher\n02=ChemLauncher\n03=VulcanTower\n04=EMPulseWeapon\n05=LaserFire\n06=LaserFire2\n07=RedEye2\n08=RPGTower\n09=90mm\n10=120mmx\n11=155mm\n12=Bomb\n13=Proton\n14=HarpyClaw\n15=Hellfire\n16=MammothTusk\n17=120mm\n18=BikeMissile\n19=HoverMissile\n20=SonicZap\n21=Dragon\n22=MechRailgun\n23=AssaultCannon\n24=RepairBullet\n25=FireballLauncher\n26=RaiderCannon\n27=SlimeAttack\n28=SuicideBomb\n29=Minigun\n30=M1Carbine\n31=Grenade\n32=BAZOOKA\n33=Heal\n34=MultiCluster\n35=Vulcan\n36=JumpCannon\n37=FiendShard\n38=CyCannon\n39=Sniper\n40=LtRail\n41=Vulcan3\n42=Pistola\n43=DropGun\n44=Jugg90mm\n45=LIMP\n46=AALaserFire\n47=CABLaser\n48=QuadLauncher\n49=WebLauncher\n50=Tentacle\n51=DEFOB\n52=DualRockets\n53=MobileEMPulseWeapon\n\n;Light infantry Rifle\n[Minigun]\nDamage=8\nROF=21\nRange=4\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=INFGUN3,GOSTGUN1,SLVKGUN1\n\n;Rocket Infantry\n[BAZOOKA]\nDamage=25\nROF=60\nRange=6\nProjectile=AAHeatSeeker2\nSpeed=25\nWarhead=AP\nReport=RKETINF1\n\n;Jump Jet Cannon\n[JumpCannon]\nDamage=15 ; was 20\nBurst=2\nROF=40\nRange=5  ; was 4\nProjectile=Invisible3\nSpeed=100\nWarhead=SA\nReport=JUMPJET1\n;Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\n\n;Assault Suit Cannon\n[AssaultCannon]\nDamage=40\nROF=50\nRange=5\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=TSGUN4\n\n;Cyborg Commando Plasma Cannon\n[CyCannon]\nDamage=120\nROF=50\nRange=7\nProjectile=ProtonBlast\nSpeed=70\nWarhead=PlasmaWH\nReport=scrin5b\n\n;Harpy Vulcan Cannon\n[HarpyClaw]\nDamage=60\nROF=36\nRange=5\nProjectile=Invisible2\nSpeed=100\nWarhead=SA\nReport=CYGUN1\n\n;Assault Buggy Cannon\n[RaiderCannon]\nDamage=40\nROF=55\nRange=4\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=CHAINGN1\nAnim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\n\n;Vulcan Tower Cannon\n[VulcanTower]\nDamage=18\nROF=26\nRange=6\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=CHAINGN1\nAnim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\nBright=true\n\n;Rocket Tower Grenade\n[RPGTower]\nDamage=110\nROF=80\nRange=8\nProjectile=Lobbed2\nSpeed=30 ; 5\nWarhead=RPG\nMinimumRange=2\nReport=GLNCH4\n\n;Bike Missile\n[BikeMissile]\nDamage=40\nROF=60\nRange=5\nProjectile=HeatSeeker\nSpeed=30\nReport=MISL1\nWarhead=AP\n\n;SAM missile\n[RedEye2]\nDamage=33\nROF=55\nRange=15\nProjectile=AAHeatSeeker\nSpeed=30\nWarhead=SAMWH\nReport=SAMSHOT1\nTurboBoost=yes\n\n;Hover Missile\n[HoverMissile]\nDamage=30\nROF=68\nRange=8\nBurst=2\nProjectile=AAHeatSeeker2\nSpeed=30\nWarhead=AP\nReport=HOVRMIS1\nMinimumRange=2\n\n;Ghost's Rail Gun\n[LtRail]\nDamage=0\t\t\t; this should be 0 for railgun shots\nAmbientDamage=150\t; use this for the railgun damage field.  Leave damage = 0\nROF=60\t\t; ROF for railgun is tied to the duration (MaxEC) of the railgun particle\nRange=6\nProjectile=Invisible\nSpeed=100\nWarhead=RailShot2\nAnim=GUNFIRE\nIsRailgun=true\nAttachedParticleSystem=SmallRailgunSys\nReport=BIGGGUN1\n\n;Mammoth Rail Gun\n[MechRailgun]\nAmbientDamage=200\t; use this for the railgun damage field.  Leave damage = 0\nDamage=0\t\t\t; this should be 0 for railgun shots\nROF=60\t\t; ROF for railgun is tied to the duration (MaxEC) of the railgun particle\nRange=8\nProjectile=Invisible\nSpeed=100\nWarhead=RailShot\nReport=RAILUSE5\nAnim=GUNFIRE\nIsRailgun=true\nAttachedParticleSystem=LargeRailgunSys\nBurst=2\t;Crim: allows both lateral firing offsets\n\n; rapid fire machine gun\n[Vulcan]\nDamage=20\nROF=60\nRange=4\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=CHAINGN1\nAnim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\n\n;Cyborg's Vulcan cannon\n[Vulcan3]\nDamage=10\nROF=30\nBurst=3\nRange=4\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=CYGUN1\n;Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW\n\n; fireball from flame tank\n[FireballLauncher]\nDamage=0\nAmbientDamage=2\nROF=50\nRange=4.25\nProjectile=Invisible\nSpeed=1\nWarhead=Fire\nReport=FLAMTNK1\nUseFireParticles=yes\nAttachedParticleSystem=FireStreamSys\nBurst=2\n\n; sniper rifle\n[Sniper]\nDamage=150\nROF=60\nRange=6.75\nProjectile=Invisible\nSpeed=100\nWarhead=HollowPoint\nReport=SILENCER\n\n; rifle soldier weapons (multiple shots)\n[M1Carbine]\nDamage=15\nROF=20\nRange=4\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=INFGUN3\n\n; man-packed anti-tank missile (bazooka type)\n[Dragon]\nDamage=30\nROF=50\nBurst=2\nRange=6\nProjectile=AAHeatSeeker2\nSpeed=25\nWarhead=AP\nReport=MISL1\n\n; air-to-surface homing missile (launched from helicopter)\n[Hellfire]\nDamage=30 ; 25\nROF=50\nRange=6\nProjectile=AAHeatSeeker2 ; was HeatSeeker\nSpeed=30\nWarhead=ORCAAP\nReport=ORCAMIS1\nBurst=2\n\n[Proton]\nDamage=20\nROF=3\nRange=5\nProjectile=ProtonTorpedo\nSpeed=30\nWarhead=AP\nReport=scrin5b\n\n; hand grenade (discus)\n[Grenade]\nDamage=40\nROF=60\nRange=4.5\nProjectile=Lobbed\n;Floater=yes\nSpeed=5\nWarhead=HE\n;Lobber=yes\nBright=yes\n\n; small anti-armor cannon\n[75mm]\nDamage=35\nROF=40\nRange=6\nProjectile=Cannon\nSpeed=40\nWarhead=AP\nAnim=GUNFIRE\nBright=yes\n\n; light anti-armor cannon\n[90mm]\nDamage=36\nROF=50\nRange=6.75\nProjectile=Cannon\nSpeed=40\nWarhead=AP\nReport=120MMF\nAnim=GUNFIRE\nBright=yes\n\n; large anti-armor cannon (two shooter)\n[120mmx]\nDamage=50\nROF=80\nRange=6.75\nProjectile=Cannon\nSpeed=40\nWarhead=AP\nReport=120MMX9\nAnim=GUNFIRE\nBurst=2\nBright=yes\n\n; large anti-armor cannon (single shooter)\n[120mm]\nDamage=70\nROF=80\nRange=6.75\nProjectile=Invisible\nSpeed=90\nWarhead=AP\nReport=120MMF\nAnim=GUNFIRE\nBright=yes\n\n[Bomb]\nDamage=160\nROF=10  ; was 1\nRange=5\nProjectile=Cannon2\nSpeed=0\nWarhead=ORCAHE\nFloater=yes\n\n[SuicideBomb]\nDamage=11000\nROF=1\nRange=.5\nProjectile=Invisible\nSpeed=0\nWarhead=Super\nBright=yes\nReport=HUNTER2\n\n; Vehicle carried anti-tank missile\n[MammothTusk]\nDamage=40\nROF=80\nRange=6\nProjectile=AAHeatSeeker\nSpeed=20\nWarhead=HE\nBurst=2\nReport=MISL1\n\n; artillery cannon\n[155mm]\nDamage=115\t;150\nROF=150\t;110\nRange=18\nMinimumRange=5\nProjectile=Ballistic\nSpeed=10\nWarhead=ARTYHE\nReport=120MMF\nAnim=GUNFIRE\nLobber=yes\n\n[Jugg90mm]\nDamage=75\nROF=150  ;was 110\nRange=18\nMinimumRange=5\nProjectile=Ballistic2\nSpeed=10\nWarhead=ARTYHE\nReport=JUGGER1\nAnim=GUNFIRE\nLobber=yes\nBurstDelay0=0\nBurstDelay1=24\nBurst=3\n\n; Sonic Zap\n[SonicZap]\nDamage=1\nAmbientDamage=3\nROF=120\nRange=6\nProjectile=Null\nSpeed=100\nWarhead=SonicWarhead\nReport=SONIC4\nIsSonic=Yes\n\n; repair bot repairing\n[RepairBullet]\nDamage=-50\nROF=80\nRange=1.8\nProjectile=Invisible\nSpeed=100\nWarhead=Mechanical\nReport=REPAIR11\nUseSparkParticles=yes\nAttachedParticleSystem=WeldingSys\n\n; medic healing\n[Heal]\nDamage=-50\nROF=80\nRange=2.83\nProjectile=Invisible\nSpeed=100\nWarhead=Organic\nReport=HEALER1\n\n; rapid fire machine gun\n[Vulcan2]\nDamage=50\nROF=50\nRange=6\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=TSGUN4\nAnim=GUNFIRE\n\n; Drop Pod Gun\n[DropGun]\nDamage=50\nROF=50\nRange=6\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=TSGUN4\nAnim=GUNFIRE\n\n; Solid laser beam.\n[LaserFire]\nDamage=250\nROF=120\nRange=10.5\nSpeed=100\nWarhead=Super\nReport=OBELRAY1\nLaserInnerColor = 255,0,0\nLaserOuterColor = 0,0,0\nLaserOuterSpread= 20,40,40\nLaserDuration = 15\nProjectile=LLine\nIsBigLaser=true\nIsLaser=true\t; this flag tells the game to use the special laser draw effect\nCharges=yes\t\t; non-charging lasers should use LaserFire2 instead\n\n; Laser Turret Beam\n[LaserFire2]\nDamage=30\nROF=40\nRange=5.5\nSpeed=100\nWarhead=Super\nReport=LASTUR1\nCharges=no\nLaserInnerColor = 255,0,0\nLaserOuterColor = 0,0,0\nLaserOuterSpread= 20,40,40\nLaserDuration = 15\nProjectile=LLine2\nIsLaser=true\t; this flag tells the game to use the special laser draw effect\n\n;Core Defender Obelisk\n[DEFOB]\nDamage=350\nROF=20\nBurst=2\nRange=10.5\nSpeed=100\nWarhead=Super2\nReport=OBELCOR3\nLaserInnerColor = 0,0,255\nLaserOuterColor = 0,0,0\nLaserOuterSpread= 20,40,40\nLaserDuration = 2 ;was 15\nProjectile=LLine\nIsBigLaser=true\nIsLaser=true\t; this flag tells the game to use the special laser draw effect\nCharges=yes\t\t; non-charging lasers should use LaserFire2 instead\n\n; Solid AA laser beam.\n[AALaserFire]\nDamage=250\nROF=20\nRange=12  ;was 10.5\nSpeed=100\nWarhead=Super\nReport=OBELMOD1\nLaserInnerColor = 0,0,255\nLaserOuterColor = 0,0,0\nLaserOuterSpread= 20,40,40\nLaserDuration = 15\nProjectile=AALLine\nIsBigLaser=true\nIsLaser=true\t; this flag tells the game to use the special laser draw effect\nCharges=yes\t\t; non-charging lasers should use LaserFire2 instead\n\n\n;Advance CABAL Obelisk Laser\n[CABLaser]\nDamage=100\nROF=70\nRange=10.5\nSpeed=100\nWarhead=Super\nReport=OBELRAY1\nLaserInnerColor = 0,0,255\nLaserOuterColor = 0,0,0\nLaserOuterSpread= 20,40,40\nLaserDuration = 15\nProjectile=LLine\nIsBigLaser=true\nIsLaser=true\t; this flag tells the game to use the special laser draw effect\nCharges=yes\t\t; non-charging lasers should use LaserFire2 instead\n\n[EMPulseWeapon]\nDamage=1200       ; Damage is duration for EM Pulse\nROF=1\nSpeed=25\nWarhead=EMPuls\nProjectile=PulsPr\nRange=40 ; was 30\nLobber=yes\nReport=PLSECAN2\n\n; Do NOT give this weapon to any units or all hell will break loose.\n[MobileEMPulseWeapon]\nDamage=1200       ; Damage is duration for EM Pulse\nROF=1\nSpeed=25\nWarhead=MobileEMPulse\nProjectile=PulsPr\nRange=40 ; was 30\nLobber=yes\nReport=PLSECAN2\n\n; Visceroid attack\n[SlimeAttack]\nDamage=100\nROF=80\nRange=1.3\t\t;2.83\nProjectile=Invisible\nSpeed=25\t;100\nWarhead=Slimer\nReport=VICER1\n\n; Chemical missile launcher\n[ChemLauncher]\nDamage=100\nROF=1 ; 0\nRange=6\nProjectile=ChemMissile\nSpeed=30\nWarhead=Gas\nReport=ICBM1\n\n[FiendShard]\nDamage=35\nROF=30\nBurst=3\nRange=5\nProjectile=DogShard\nSpeed=25\nWarhead=Shard\nReport=FIEND2\n\n[MultiLauncher]\nDamage=130\nROF=80\nRange=30\nProjectile=MultiMissile\nSpeed=35  ; was 10\nWarhead=HE\nReport=SAMSHOT1\n\n[Pistola]\nDamage=2\nROF=20\nRange=3\nProjectile=Invisible\nSpeed=100\nWarhead=SA\nReport=GUN18\n\n; MultiMissile Cluster Missiles\n[MultiCluster]\nDamage=65\nROF=80\nRange=6\nProjectile=HeatSeeker\nSpeed=20\nWarhead=HE\nBurst=2\nReport=MISL1\n\n; Limpet Drone Weapon\n[LIMP]\nDamage=1\nROF=80\nRange=2\nProjectile=LimpetBullet\nSpeed=100\nWarhead=LIMPY\nReport=LIMPBOM1\n\n[WebLauncher]\nDamage=0\nROF=200  ;was 180\nRange=7\nProjectile=WebCapsule\nSpeed=25  ; was 10\nWarhead=WebMass\nReport=FIREWEB1\nAttachedParticleSystem=SmallGreySSys\n\n; Second Stage of Cyborg Spider rocket launcher\n[DualRockets]\nDamage=5 ;was 4\nROF=180  ;was 80\nRange=6\nProjectile=AAHeatSeeker2\nSpeed=15\nWarhead=AP\nBurst=2\nReport=RKETINF1\n\n; First stage of Cyborg Spider rocket launcher\n[QuadLauncher]\nDamage=0\nROF=180  ;was 80\nRange=7\nProjectileRange=2\nMinimumRange=3  ; was 2\nProjectile=DualCluster\nSpeed=25  ; was 10\nWarhead=SA\nReport=SAMSHOT1\nBurst=2\n\n; Jellyfish Tentacle\n[Tentacle]\nDamage=16\nROF=80\nRange=15\nProjectile=Invisible\nSpeed=25\t; Not used; see levitation parameters\nWarhead=Stinger\nReport=FLOATK1\n\n\n\n; ******* Projectile Statistics *******\n; Projectiles describe how and what image to use as the weapon flies\n; to its target. Think of the projectile as the \"delivery method\" used\n; to get the warhead to the desired target.\n\n; AA = Can this weapon fire upon flying aircraft (def=no)?\n; AG = Can this weapon fire upon ground objects (def=yes)?\n; ASW = Is this an Anti-Submarine-Warfare projectile (def=no)?\n; Acceleration = amount to increase missile speed (def=3)\n; Airburst = Does it try to fly over the target instead of hit it (def=no)?\n; Arm = arming delay (def=0)\n; Bouncy = Does it bounce a bit upon impact (def=no)?\n; Degenerates = Does the bullet strength weaken as it travels (def=no)?\n; Dropping = Does it fall from a starting height (def=no)?\n; Elasticity = \"bounciness\" of the object [should be 0.0 through 1.0] (def=0.75)\n; High = Can it fly over walls (def=no)?\n; Image = image to use during flight\n; Inaccurate = Is it inherently inaccurate (def=no)?\n; Inviso = Is the projectile invisible as it travels (def=no)?\n; Parachuted = Equipped with a parachute for dropping from plane (def=no)?\n; Proximity = Does it blow up when near its target (def=no)?\n; ROT = Rate Of Turn [non zero implies homing] (def=0)\n; Ranged = Can it run out of fuel (def=no)?\n; Shadow = If High, does this bullet need to have a shadow drawn? (def = yes)\n; Color = Color scheme to use for special remapping projectiles (def = none)\n; VeryHigh = Does it fly at a very high cruise altitude (def=no)?\n; Cluster = number of explosions attached to projectile [cluster-bomb] (def=1)\n\n; invisible flight to target\n[Invisible]\nInviso=yes\nImage=none\n\n; Harpy Claw tracking projectile\n[Invisible2]\nInviso=yes\nImage=none\nROT=3\nAA=yes\nAG=yes\n\n; Jump jet cannon\n[Invisible3]\nInviso=yes\nImage=none\nAA=yes\nAG=yes\n\n[Null]\nInviso=yes\nArm=9999999\nImage=none\n\n; straight high-speed ballistic shot\n[Cannon]\nImage=120MM\nArcing=true\n\n; orca bomber bomblets\n[Cannon2]\nImage=120MM\nAA=no\n\n; chemical missile\n[ChemMissile]\nArm=2\nHigh=yes\nVeryHigh=yes\nCluster=8\n;Shadow=no\nProximity=yes\nRanged=yes\nAA=no\nImage=MISLCHEM\nROT=4\nColor=DarkGreen\nIgnoresFirestorm=yes\n\n[MultiMissile]\nArm=2\nHigh=yes\nVeryHigh=yes\n;Shadow=no\nProximity=yes\nCluster=10          ; number of small missiles to launch\nRanged=yes\nAA=no\nImage=MISLMLTI\nROT=4\nColor=DarkGreen\nAirburst=yes\nAirburstWeapon=MultiCluster\nIgnoresFirestorm=yes\n\n; small homing missile (targets vehicles best)\n[HeatSeeker]\nArm=2\nHigh=yes\nShadow=no\nProximity=yes\nRanged=yes\nImage=DRAGON\nROT=8\n\n[ProtonTorpedo]\nArm=2\nHigh=yes\nShadow=no\nProximity=yes\nRanged=yes\nImage=TORPEDO\nROT=1\nIgnoresFirestorm=yes\n\n[ProtonBlast]\nHigh=yes\nShadow=no\nProximity=yes\nRanged=yes\nImage=TORPEDO\nROT=1\nIgnoresFirestorm=yes\n\n; aircraft-only heatseeker\n[AAHeatSeeker]\nArm=2\nHigh=yes\nShadow=no\nProximity=yes\nRanged=yes\nAA=yes\nAG=no\nImage=DRAGON\nROT=5\n\n; aircraft and ground heatseeker\n[AAHeatSeeker2]\nArm=2\nHigh=yes\nShadow=no\nProximity=yes\nRanged=yes\nAA=yes\nAG=yes\nImage=DRAGON\nROT=8\n\n[Lobbed]\nHigh=yes\nImage=DISCUS\nBouncy=yes\nArcing=yes\nFloater=yes\n\n[Lobbed2]\nHigh=yes\nImage=CANISTER\nArcing=true\n\n; arcing ballistic projectile\n[Ballistic]\nHigh=yes\nImage=120MM\nArcing=true\n\n; arcing ballistic projectile\n[Ballistic2]\nHigh=yes\nImage=120MM\nArcing=true\nInaccurate=true\nBouncy=yes\nElasticity=0.0\n\n[PulsPr]\nHigh=yes\nImage=PULSBALL\n\n[LLine]\nInviso=yes\nImage=none\nAA=no\nAG=yes\n\n[LLine2]\nInviso=yes\nImage=none\nAA=no\nAG=yes\n\n[AALLine]\nInviso=yes\nImage=none\nAA=yes\nAG=no\n\n[DogShard]\nImage=CRYSTAL4\nArcing=true\n\n[WebCapsule]\nArm=2\nHigh=no\nShadow=no\nProximity=yes\nRanged=yes\nAA=no\nAG=yes\nImage=WEB\nROT=5\n\n; Cyborg Spider multi-missile\n[DualCluster]\nHigh=no\nVeryHigh=no\n;Shadow=no\nProximity=no\nCluster=2          ; number of small missiles to launch\nRanged=yes\nRange=3\nAA=yes\nImage=DRAGON\nROT=4\nColor=DarkGreen\nSplits=yes\nAirburstWeapon=DualRockets\nIgnoresFirestorm=yes\nRetargetAccuracy=75%\n\n; Limpet Projectile\n[LimpetBullet]\nInviso=yes\nImage=none\nAV=true\n\n\n\n; *** Particle Systems ***\n; This is a list of the various types of particles systems available in the game\n[ParticleSystems]\n1=GasCloudSys\n2=FireStreamSys\n3=BigGreySmokeSys\n4=SmallGreySSys\n5=DebrisSmokeSys\n6=SparkSys\n7=FirestormSparkSys\n8=TestSmokeSys\n9=SmallRailgunSys\n10=LargeRailgunSys\n11=WeldingSys\n12=LGSparkSys\n13=WebSys\n14=GasPuffSys\n15=SmokeStackSys\n\n; HoldsWhat = type of particle (see below) that this system manages (required)\n; Spawns = does this system spawn particles by itself (def = no)\n; SpawnFrames = number of frames to wait before spawning another particle\n; ParticleCap = maximum number of particles that can be in this system\n\n[SmallRailgunSys]\nHoldsWhat=SmallRailgunPart\nBehavesLike=Railgun\nSpiralRadius=6\nParticlesPerCoord=.1\nSpiralDeltaPerCoord=.035\nMovementPerturbationCoefficient=.3\nPositionPerturbationCoefficient=20\nVelocityPerturbationCoefficient=.6\nLaser=yes\nLaserColor=255,128,0\n\n[LargeRailgunSys]\nHoldsWhat=LargeRailgunPart\nBehavesLike=Railgun\nSpiralRadius=15\nParticlesPerCoord=.15\nSpiralDeltaPerCoord=.03\nMovementPerturbationCoefficient=.4\nPositionPerturbationCoefficient=30\nVelocityPerturbationCoefficient=.6\nLaser=yes\nLaserColor=25,20,255\n;R,G,B for laser color\n\n[WeldingSys]\nHoldsWhat=WeldingSpark\nBehavesLike=Spark\nParticleCap=25\nSparkSpawnFrames=20\nLightSize=25\nOneFrameLight=true\nSpawnSparkPercentage=.4\n\n[SparkSys]\nHoldsWhat=Spark\nBehavesLike=Spark\nParticleCap=12\nSparkSpawnFrames=1\nLightSize=21\nSpawnSparkPercentage=1\n\n[FirestormSparkSys]\nHoldsWhat=FirestormSpark\nBehavesLike=Spark\nParticleCap=25\nSparkSpawnFrames=1\nLightSize=21\nSpawnSparkPercentage=1\n\n; this is the global gas system\n[GasCloudSys]\nHoldsWhat=GasCloud1\nBehavesLike=Gas\n\n; a system for large amounts of smoke (damaged buildings, destroyed things)\n[BigGreySmokeSys]\nHoldsWhat=LargeGreySmoke\nSpawns=yes\nSpawnFrames=10\nSpawnRadius=10\nSlowdown=.0025\nParticleCap=30\nSpawnCutoff=15.0\nSpawnTranslucencyCutoff=13.0\nBehavesLike=Smoke\n\n; a system for small amounts of smoke (damaged units)\n[SmallGreySSys]\nHoldsWhat=SmallGreySmoke\nSpawns=yes\nSpawnFrames=5\nSpawnRadius=5\nSlowdown=.0025\nParticleCap=15\nSpawnCutoff=13.0\nSpawnTranslucencyCutoff=12.5\nBehavesLike=Smoke\n\n; a system for small amounts of smoke (damaged units)\n[TestSmokeSys]\nHoldsWhat=TestSmoke\nSpawns=yes\nSpawnFrames=10\nSpawnRadius=5\nSlowdown=.0025\nParticleCap=15\nSpawnCutoff=13.0\nSpawnTranslucencyCutoff=12.5\nBehavesLike=Smoke\n\n[DebrisSmokeSys]\nHoldsWhat=SmallGreySmoke\nSpawns=yes\nSpawnFrames=2\nSpawnRadius=3\nParticleCap=15\nSpawnCutoff=13.0\nSpawnTranslucencyCutoff=13.0\nBehavesLike=Smoke\n\n[FireStreamSys]\nHoldsWhat=FireStream\nSpawns=yes\nSpawnFrames=4\nBehavesLike=Fire\nImage=TWLT036\nLifetime=30 ; was 100\n\n[LGSparkSys]\nHoldsWhat=LargeSpark\nBehavesLike=Spark\nParticleCap=15\nSparkSpawnFrames=5\nLightSize=25\nOneFrameLight=true\nSpawnSparkPercentage=.2\n\n[WebSys]\nHoldsWhat=Web\nBehavesLike=Web\nParticleCap=20\nSpawnRadius=10\nLifetime=30\nSpawns=no\nSpawnFrames=10\nSlowdown=0.05\nSpawnCutoff=15.0\nSpawnTranslucencyCutoff=13.0\n\n[GasPuffSys]\nHoldsWhat=WeakGasCloud\nBehavesLike=WeakGas\nLifetime=3 ; 30\n\n[SmokeStackSys]\nHoldsWhat=SmokeStackPuff\nSpawns=yes\nSpawnFrames=2\nSpawnRadius=3\nParticleCap=15\nSpawnCutoff=13.0\nSpawnTranslucencyCutoff=13.0\nBehavesLike=Smoke\nLifetime=75\n\n\n\n; *** Particles ***\n;  This is a list of the various particle types in the game\n;  These are usually objects of gassy nature: poison gas, smoke, fire, etc...\n[Particles]\n; These first three must be in this order!\n1=GasCloud1\n2=GasCloud2\n3=FireStream\n4=Spark\n5=FirestormSpark\n6=LargeGreySmoke\n7=SmallGreySmoke\n8=TestSmoke\n9=GasCloudD1\n10=GasCloudD2\n11=SmallRailgunPart\n12=LargeRailgunPart\n13=GasCloudM1\n14=GasCloudM2\n15=WeldingSpark\n16=LargeSpark\n17=Web\n18=WeakGasCloud\n19=WeakGasCloudD\n20=SmokeStackPuff\n21=WeakGasCloudM2\n\n; Image = Imagelist to use for particle\n; Persistent = Does this particle stick around; should always be yes\n; MaxDC = How many frames go by before this particle damages the things near it? (def = 0)\n; MaxEC = How many frames does this object last (def = 0)\n; Damage = How much damage does it do (def = 0)\n; Warhead = What warhead to use for damage purposes (def = WARHEAD_NONE)\n; StartFrame = what frame of image to start on? (def = 0)\n; NumLoopFrames = how many frames form a single loop? (def = 1)\n; WindEffect = to what degree does the wind affect his particle, 0 = not at all, 5 = a lot (def = 0)\n; Velocity = speed at which particle travels (def = 0.0)\n; Radius = how big is this particle? (used for attract/repel)\n;\n; BehavesLike, DeleteOnStateLimit, EndStateAI, StateAIAdvance are things that\n; shouldn't be messed with\n\n; the particle that makes up the fire stream of flamethrowers, and flame tanks\n[FireStream]\nImage=FLAMEALL\nDeacc=0.01\nVelocity=28.0\nBehavesLike=Fire\nMaxEC=500\nMaxDC=3\nWarhead=Fire\nDamage=2\nStartStateAI=1\nEndStateAI=19\nStateAIAdvance=6\nTranslucent50State=15\nTranslucent25State=10\nDeleteOnStateLimit=yes\nNormalized=yes\nFinalDamageState=14\nReport=FLAMTNK1\n\n[WeldingSpark]\nBehavesLike=Spark\nMaxEC=500\nXVelocity=16\nYVelocity=16\nMinZVelocity=40\nZVelocityRange=15\nColorList=(0,128,255),(255,255,255),(200,200,150),(80,80,80),(0,0,0)\nStartColor1=80,255,255\nStartColor2=255,255,100\nColorSpeed=.13\n\n[Spark]\nBehavesLike=Spark\nMaxEC=500\nXVelocity=10\nYVelocity=10\nMinZVelocity=40\nZVelocityRange=15\nColorList=(255,255,255),(200,200,80),(200,10,10),(0,0,0)\nColorSpeed=.13\n\n[FirestormSpark]\nBehavesLike=Spark\nMaxEC=500\nXVelocity=16\nYVelocity=16\nMinZVelocity=40\nZVelocityRange=15\nColorList=(0,0,255),(255,255,255),(200,200,80),(200,10,10),(0,0,0)\nColorSpeed=.13\n\n; Cloud of Poison Gas #1 Formation particle\n[GasCloudM1]\nImage=gaslrgmk\nMaxDC=60\nMaxEC=448\nDamage=0\nWarhead=Gas\nStartFrame=0\nEndStateAI=11\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=3\nNextParticle=GasCloud1\nDeleteOnStateLimit=yes\nNextParticleOffset=0,0,150\n\n; Cloud of Poison Gas #2 formation particle\n[GasCloudM2]\nImage=gaslrgmk\nMaxDC=60\nMaxEC=448\nDamage=0\nWarhead=Gas\nStartFrame=0\nEndStateAI=11\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=3\nNextParticle=GasCloud2\nDeleteOnStateLimit=yes\nNextParticleOffset=0,0,150\n\n; Cloud of Poison Gas #1\n[GasCloud1]\nImage=CLOUD1\nMaxDC=60\nMaxEC=1000\nDamage=50\nWarhead=Gas\nStartFrame=0\nEndStateAI=28\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=4\nNextParticle=GasCloudD1\n\n; Cloud of Poison Gas #2\n[GasCloud2]\nImage=CLOUD2\nMaxDC=60\nMaxEC=1000\nDamage=40\nWarhead=Gas\nStartFrame=0\nEndStateAI=28\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=4\nNextParticle=GasCloudD2\n\n; Cloud of Poison Gas #1 Dissipation particle\n[GasCloudD1]\nImage=CLOUD1D\nMaxDC=60\nMaxEC=50\nDamage=10\nWarhead=Gas\nStartFrame=0\nEndStateAI=12\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=4\nDeleteOnStateLimit=yes\n\n; Cloud of Poison Gas #2 dissipating\n[GasCloudD2]\nImage=CLOUD2D\nMaxDC=60\nMaxEC=50\nDamage=10\nWarhead=Gas\nStartFrame=0\nEndStateAI=12\nTranslucency=50\nWindEffect=0\nBehavesLike=Gas\nStateAIAdvance=4\nDeleteOnStateLimit=yes\n\n\n[LargeGreySmoke]\nImage=LGRYSMK1\nMaxEC=80\nTranslucency=25\nVelocity=8.0\nDeacc=.05\nWindEffect=0\nBehavesLike=Smoke\nDeleteOnStateLimit=yes\nEndStateAI=20\nStateAIAdvance=4\n\n[SmallGreySmoke]\nImage=SGRYSMK1\nMaxEC=80\nTranslucency=25\nVelocity=9.0\nDeacc=.05\nWindEffect=0\nBehavesLike=Smoke\nDeleteOnStateLimit=yes\nEndStateAI=20\nStateAIAdvance=4\n\n[TestSmoke]\nImage=SGRYSMK1\nMaxEC=80\nTranslucency=25\nVelocity=6.0\nDeacc=.05\nWindEffect=0\nBehavesLike=Smoke\nDeleteOnStateLimit=yes\nEndStateAI=20\nStateAIAdvance=3\n\n[SmallRailgunPart]\nBehavesLike=Railgun\nMaxEC=70\nColorList=(200,200,200),(150,150,150)\nColorSpeed=.03\nVelocity=.4\n\n[LargeRailgunPart]\nBehavesLike=Railgun\nMaxEC=70\nColorList=(25,70,205),(150,150,150)\nColorSpeed=.009\nVelocity=.3\n\n[LargeSpark]\nBehavesLike=Spark\nMaxEC=500\nXVelocity=13\nYVelocity=13\nMinZVelocity=40\nZVelocityRange=15\nColorList=(255,255,255),(200,200,80),(200,10,10),(0,0,0)\nColorSpeed=.13\n\n[Web]\nImage=Web\nBehavesLike=Web\nPersistent=true\nMaxDC=2\nMaxEC=80\nDamage=0\nWarhead=none\nTranslucency=25\nDeleteOnStateLimit=yes\nVelocity=8.0\nDeacc=.05\nWindEffect=0\nEndStateAI=10\nStateAIAdvance=2\n\n[WeakGasCloud]\nImage=CLOUD2\nMaxDC=60\nMaxEC=50 ;1000\nVelocity=8.0\nDamage=4\nWarhead=Gas\nStartFrame=0\nEndStateAI=28\nTranslucency=50\nWindEffect=0\nBehavesLike=WeakGas\nStateAIAdvance=4\nNextParticle=WeakGasCloudD\n\n[WeakGasCloudD]\nImage=CLOUD2D\nMaxDC=60\nMaxEC=10 ; 50\nVelocity=8.0\nDamage=1\nWarhead=Gas\nStartFrame=0\nEndStateAI=12\nTranslucency=50\nWindEffect=0\nBehavesLike=WeakGas\nStateAIAdvance=4\nDeleteOnStateLimit=yes\n\n[SmokeStackPuff]\nImage=SGRYSMK1\nMaxEC=80\nTranslucency=25\nVelocity=9.0\nDeacc=.05\nWindEffect=0\nBehavesLike=Smoke\nDeleteOnStateLimit=yes\nEndStateAI=20\nStateAIAdvance=4\n\n\n\n; ******* Warhead Characteristics *******\n; This is a list of the various types of warheads available in the game\n[Warheads]\n1=EMPuls\n2=SonicWarhead\n3=TankOGas\n4=SA\n5=HE\n6=AP\n7=Gas\n8=Fire\n9=HollowPoint\n10=Super\n11=Organic\n12=Slimer\n13=FirestormWH\n14=IonCannonWH\n15=RailShot\n16=Mechanical\n17=VeinholeWH\n18=IonWH\n19=ARTYHE\n20=PlasmaWH\n21=SAMWH\n22=ORCAAP\n23=RailShot2\n24=ORCAHE\n25=WebMass\n26=LIMPY\n27=CoreDefPlasmaWH\n\n; This is what gives the \"rock, paper, scissors\" character to the game.\n; It describes how the damage is to be applied to the target. The\n; values should take into consideration the 'area of effect'.\n; example: Although an armor piercing tank round would instantly\n; kill a soldier IF it hit, the anti-infantry rating is still\n; very low because the tank round has such a limited area of\n; effect, lacks pinpoint accuracy, and acknowledges the fact that\n; tanks pose little threat to infantry that take cover.\n\n; Spread = damage spread factor [larger means greater spread] (def=1)\n;          [A value of 1 means the damage is halved every pixel distant from center point.\n;          a value of 2 means damage is halved every 2 pixels, etc.]\n; Wall = Does this warhead damage concrete walls (def=no)?\n; Wood = Does this warhead damage wood walls (def=no)?\n; Fire = Does this produce great heat and thus will melt ice (def=no)?\n; Tiberium = Does this warhead destroy tiberium (def=no)?\n; Sparky = Does this warhead cause residual flames (def=no)?\n; Conventional = Is explosive warhead big enough to cause a splash when it hits water [nukes are too big for this] (def=no)?\n; Rocker = Can this warhead cause nearby units to rock upon impact (def=no)?\n; AnimList = list of animations to play when warhead explodes [listed from lesser to greater damage]\n; Verses = damage value verses various armor types (as percentage of full damage)...\n;           -vs- none, wood (buildings), light armor, heavy armor, concrete\n; InfDeath = which infantry death animation to use (def=0)\n;             0=instant die, 1=twirl die, 2=explodes, 3=flying death, 4=burn death, 5=electro\n; Deform = % chance that this warhead will damage the ground when it hits. (def=0)\n; DeformThreshhold = damage must exceed this amount before deformation can occur (def=0)\n; Particle = Particle effect to use when explosion occurs (def=none)\n; ProneDamage = Damage modifer for infantry when prone (def=1.0)\n; 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.\n\n; EM Pulse cannon warhead.\n[EMPuls]\nSpread=11       ; Spread is radius of EM pulse effect.\nEMEffect=yes\nAnimList=PULSEFX1,PULSEFX2\n\n[MobileEMPulse]\nSpread=6  ;was 8\nEMEffect=yes\nAnimList=MEMPFX ; PULSEFX1,PULSEFX2\n\n; warhead for the sonic zap\n[SonicWarhead]\nSpread=2\nWood=yes\nVerses=100%,100%,100%,80%,60%  ; was 65, 50\nInfDeath=3\nRocker=yes\nProneDamage=50%\n\n; warhead for the flying tank of gas\n[TankOGas]\nSpread=8\nWall=yes\nWood=yes\nTiberium=yes\nSparky=yes\nRocker=no\nAnimList=TWLT026,TWLT036,TWLT050,TWLT070,TWLT100\nVerses=90%,100%,60%,25%,10%\nFire=yes\nInfDeath=4\nProneDamage=50%\n\n[RailShot]\nSpread=1\nVerses=200%,175%,160%,100%,25%\nRocker=no\nProneDamage=100%\nInfDeath=2\n\n; Ghost's Railgun\n[RailShot2]\nSpread=1\nVerses=100%,130%,150%,110%,5%\nRocker=no\nProneDamage=100%\nInfDeath=2\n\n; general multiple small arms fire\n[SA]\nSpread=3\nVerses=100%,60%,40%,25%,10%\nInfDeath=1\nAnimList=PIFFPIFF,PIFFPIFF\nBright=yes\nProneDamage=70%\n\n; high explosive (shrapnel)\n[HE]\nSpread=4\nWall=yes\nWood=yes\nVerses=100%,85%,70%,35%,28%   ; changed conc from 10%\nConventional=yes\nRocker=no\nInfDeath=2\nAnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG,TWLT070\nDeform=10%\nDeformThreshhold=300\nTiberium=yes\nSparky=yes\nBright=yes\nProneDamage=70%     ; Presumes air burst\n\n; Artillery shell\n[ARTYHE]\nSpread=6\nWall=yes\nWood=yes\nVerses=40%,85%,68%,35%,35%\nConventional=yes\nRocker=yes\nInfDeath=2\nAnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG\nDeform=15%\nDeformThreshhold=120\nTiberium=yes\nBright=yes\nProneDamage=30%\t;150%\n\n; Ion storm strike\n[IonWH]\nSpread=6\nWall=yes\nWood=yes\nVerses=90%,75%,60%,25%,100%\nConventional=yes\nRocker=yes\nInfDeath=5\nAnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG\nDeform=10%\nDeformThreshhold=300\nTiberium=yes\nSparky=yes\nBright=yes\nProneDamage=75%     ; Presumes air burst\n\n; armor piercing (discarding sabot, narrow effect)\n[AP]\nSpread=3\nWall=yes\nWood=yes\nVerses=25%,65%,75%,100%,60%\nConventional=yes\nInfDeath=3\nAnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58\nProneDamage=50%\n\n; RPG Warhead\n[RPG]\nSpread=3\nWall=yes\nWood=yes\nRocker=yes\nVerses=30%,75%,90%,100%,70%\nConventional=yes\nInfDeath=3\nAnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58\nProneDamage=100%\n\n; Poison Gas Cloud\n[Gas]\nSpread=512\nVerses=200%,150%,100%,20%,0%\nInfDeath=1\nParticle=GasCloudSys\nProneDamage=300%    ; Gas concentrates at gound level\n\n[WeakGas]\nSpread=512\nVerses=100%,0%,0%,0%,0%\nInfDeath=1\n;Particle=WeakGasCloudSys\nProneDamage=300%    ; Gas concentrates at ground level\n\n; napalm and fire in general\n[Fire]\nSpread=8\nWood=yes\nVerses=600%,148%,59%,6%,2%\nInfDeath=4\nSparky=yes\nFire=yes\nProneDamage=600%\n\n; napalm and fire in general, that doesn't set other things on fire (weird, but necessary)\n[Fire2]\nSpread=8\nWood=yes\nVerses=600%,148%,59%,6%,2%\nInfDeath=4\nSparky=no\nFire=yes\nProneDamage=600%\n\n; anti-infantry rifle bullet (single shot -- very effective verses infantry)\n[HollowPoint]\nSpread=1\nVerses=100%,5%,5%,5%,5%\nInfDeath=1\nAnimList=PIFF\nProneDamage=100%\n\n; special case damage effect (do not use for regular weapons)\n[Super]\nSpread=0\nVerses=100%,100%,100%,100%,100%\nInfDeath=5\nTiberium=yes\nProneDamage=60%\nSparky=yes\n\n[Super2]\nSpread=0\nVerses=100%,100%,100%,100%,100%\nInfDeath=5\nTiberium=yes\nProneDamage=60%\nSparky=yes\nWall=yes\n\n; special case to only affect mechanical units\n[Mechanical]\nSpread=0\nVerses=0%,100%,100%,100%,100%\nInfDeath=0\n\n; special case to only affect infantry (do not use for regular weapons)\n[Organic]\nSpread=0\nVerses=100%,0%,0%,0%,0%\nInfDeath=0\n\n[Slimer]\nSpread=0\nVerses=100%,100%,60%,40%,20%\nInfDeath=1\t;Crim: was 0, causing the infantry to just disappear in thin air. Slime shouldn't do that.\n\n[Shard]\nSpread=0\nVerses=100%,100%,60%,40%,20%\nInfDeath=1\n\n[FirestormWH]\nSpread=0\nVerses=100%,100%,100%,100%,100%\nInfDeath=4\n\n[IonCannonWH]\nSpread=40\nVerses=100%,100%,100%,100%,100%\nInfDeath=5\nWood=yes\nWall=yes\nFire=yes\nDeform=100%\nSparky=yes\n\n[VeinholeWH]\nSpread=1\nVerses=100%,100%,100%,100%,100%\nInfDeath=0\nVeinhole=yes\n\n[PlasmaWH]\nSpread=0\nVerses=350%,260%,205%,150%,80%\nInfDeath=5\nWall=yes\nBright=yes\nTiberium=yes\nProneDamage=350%\nAnimList=EXPLOMED,EXPLOLRG\nSparky=yes\n\n[SAMWH]\nSpread=3\nVerses=100%,100%,100%,100%,100%\nInfDeath=3\nAnimList=XGRYSML1,XGRYSML2,EXPLOSML\nProneDamage=100%\n\n; Special Orca AP missile\n[ORCAAP]\nSpread=2\nWall=yes\nWood=yes\nVerses=30%,65%,150%,100%,30%\nConventional=yes\nInfDeath=3\nAnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58\nProneDamage=50%\n\n; Orca bomber HE bomb\n[ORCAHE]\nSpread=512\nWall=yes\nSparky=yes\nWood=yes\nBright=yes\nFire=yes\nVerses=200%,90%,75%,32%,100%   ; changed conc from 10%\nConventional=yes\nRocker=yes\nInfDeath=2\nAnimList=EXPLOMED,EXPLOLRG\nDeform=8%\nDeformThreshhold=160\nTiberium=yes\nProneDamage=150%\n\n; Limpet Warhead\n[LIMPY]\nLimpetFactor=35\nSpread=0\nVerses=0%,100%,100%,100%,100%\nInfDeath=0\n\n[WebMass]\nWood=no\nVerses=600%,0%,0%,0%,0%\nInfDeath=4\nSparky=no\nFire=no\nProneDamage=100%\nAnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG,TWLT070\nWebby=true\nWebDuration=600   ;was 300\nWebDurationVariation=25\nWebRadius=2\nParticle=WebSys\nSpread=4\n\n[Stinger]\nSpread=0\nVerses=60%,45%,90%,55%,0%\nInfDeath=5\nAnimList=PULSEFX1,PULSEFX2\nSparky=yes\nBright=yes\nProneDamage=45%\n\n\n\n; *** Terrain Objects ***\n; This is the list of terrain objects. Typically, these include\n; trees and rocks.\n[TerrainTypes]\n;1=MINE\n2=BOXES01\n3=BOXES02\n4=BOXES03\n5=BOXES04\n6=BOXES05\n7=BOXES06\n8=BOXES07\n9=BOXES08\n10=BOXES09\n11=ICE01\n12=ICE02\n13=ICE03\n14=ICE04\n15=ICE05\n16=TREE01\n17=TREE02\n18=TREE03\n19=TREE04\n20=TREE05\n21=TREE06\n22=TREE07\n23=TREE08\n24=TREE09\n25=TREE10\n26=TREE11\n27=TREE12\n28=TREE13\n29=TREE14\n30=TREE15\n31=TREE16\n32=TREE17\n33=TREE18\n34=TREE19\n35=TREE20\n36=TREE21\n37=TREE22\n38=TREE23\n39=TREE24\n40=TREE25\n41=TIBTRE01\n42=TIBTRE02\n43=TIBTRE03\n44=VEINTREE\n45=FONA01\n46=FONA02\n47=FONA03\n48=FONA04\n49=FONA05\n50=FONA06\n51=FONA07\n52=FONA08\n53=FONA09\n54=FONA10\n55=FONA11\n56=FONA12\n57=FONA13\n58=FONA14\n59=FONA15\n60=BIGBLUE3\n\n; This section lists all the terrain object types and\n; specifies their characteristics.\n\n; Immune = Is the terrain immune to combat damage (def=no)?\n; WaterBound = Is the terrain only allowed on the water (def=no)?\n; SpawnsTiberium = Does it spawn growth of Tiberium around it (def=no)?\n; IsFlammable = Can \"Forest Fires\" spread to and damage this terrain type?\n\n[BOXES01]\nName=Boxes\nImmune=yes\n\n[BOXES02]\nName=Boxes\nImmune=yes\n\n[BOXES03]\nName=Boxes\nImmune=yes\n\n[BOXES04]\nName=Boxes\nImmune=yes\n\n[BOXES05]\nName=Boxes\nImmune=yes\n\n[BOXES06]\nName=Boxes\nImmune=yes\n\n[BOXES07]\nName=Boxes\nImmune=yes\n\n[BOXES08]\nName=Boxes\nImmune=yes\n\n[BOXES09]\nName=Boxes\nImmune=yes\n\n[ICE01]\nName=Ice Floe\nImmune=yes\nWaterBound=yes\n\n[ICE02]\nName=Ice Floe\nImmune=yes\nWaterBound=yes\n\n[ICE03]\nName=Ice Floe\nImmune=yes\nWaterBound=yes\n\n[ICE04]\nName=Ice Floe\nImmune=yes\nWaterBound=yes\n\n[ICE05]\nName=Ice Floe\nImmune=yes\nWaterBound=yes\n\n[TREE01]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=6\n\n[TREE02]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE03]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=6\n\n[TREE04]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\n\n[TREE05]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=5\nSnowOccupationBits=7\n\n[TREE06]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE07]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE08]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=4\n\n[TREE09]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=4\n\n[TREE10]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=6\nSnowOccupationBits=3\n\n[TREE11]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=3\n\n[TREE12]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE13]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=4\n\n[TREE14]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=4\n\n[TREE15]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE16]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE17]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE18]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TREE19]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=2\n\n[TREE20]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=3\n\n[TREE21]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=1\n\n[TREE22]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=3\n\n[TREE23]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=3\n\n[TREE24]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=6\n\n[TREE25]\nName=Tree\nIsFlammable=yes\nRadarColor=0,192,0\nTemperateOccupationBits=4\nSnowOccupationBits=7\n\n[TIBTRE01]\nName=Tiberium Tree\nSpawnsTiberium=yes\nRadarColor=192,192,0\nIsAnimated=yes\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=1.5\nLightBlueTint=0.01\nAnimationRate=3\nAnimationProbability=.003\nImmune=yes\n\n[TIBTRE02]\nName=Tiberium Tree\nSpawnsTiberium=yes\nRadarColor=192,192,0\nIsAnimated=yes\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=1.5\nLightBlueTint=0.01\nAnimationRate=3\nAnimationProbability=.003\nImmune=yes\n\n[TIBTRE03]\nName=Tiberium Tree\nSpawnsTiberium=yes\nRadarColor=192,192,0\nIsAnimated=yes\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=0.01\nLightGreenTint=1.5\nLightBlueTint=0.01\nAnimationRate=3\nAnimationProbability=.003\nImmune=yes\n\n[BIGBLUE3]\nName=Blue Tiberium Tree\nSpawnsTiberium=yes\nTiberiumToSpawn=2\nRadarColor=192,192,0\nIsAnimated=yes\nLightVisibility=4000\nLightIntensity=0.01\nLightRedTint=1.00\nLightGreenTint=1.00\nLightBlueTint=1.00\nAnimationRate=6\nAnimationProbability=.003\nImmune=yes\n\n[VEINTREE]\nName=Veinhole Tree\nImage=None\nArmor=None\nIsVeinhole=true\nStrength=1000\n\n[FONA01]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=7\nYDrawFudge=-12\n\n[FONA02]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA03]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA04]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA05]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA06]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA07]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA08]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA09]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA10]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA11]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA12]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA13]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA14]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n[FONA15]\nName=Fona\nIsFlammable=no\nRadarColor=0,192,0\nTemperateOccupationBits=7\nSnowOccupationBits=6\nYDrawFudge=-12\n\n\n\n; *** Overlay Objects ***\n; These specify the various overlay types. Overlays can affect the\n; game state (unlike smudges).\n; Limits 000-254. 255 is reserved for no overlay\n[OverlayTypes]\n000=GASAND\n001=CYCL\n002=GAWALL\n003=BARB\n004=WOOD\n005=DUMMY\n006=DUMMY2\n007=DUMMY3\n008=DUMMY4\n009=DUMMY5\n010=DUMMY6\n011=DUMMY7\n012=DUMMY8\n013=DUMMY9\n014=DUMMY10\n015=DUMMY11\n016=DUMMY12\n017=V16\n018=V17\n019=V18\n020=DUMMY13\n021=DUMMY14\n022=FENC\n023=DUMMY15\n024=BRIDGE1      ;BRIDGE1 &\n025=BRIDGE2      ;BRIDGE2 are the same art.\n026=NAWALL\n027=BTIB01\n028=BTIB02\n029=BTIB03\n030=BTIB04\n031=BTIB05\n032=BTIB06\n033=BTIB07\n034=BTIB08\n035=BTIB09\n036=BTIB10\n037=BTIB11\n038=BTIB12\n039=TRACKS01\n040=TRACKS02\n041=TRACKS03\n042=TRACKS04\n043=TRACKS05\n044=TRACKS06\n045=TRACKS07\n046=TRACKS08\n047=TRACKS09\n048=TRACKS10\n049=TRACKS11\n050=TRACKS12\n051=TRACKS13\n052=TRACKS14\n053=TRACKS15\n054=TRACKS16\n055=TRACKTUNNEL01\n056=TRACKTUNNEL02\n057=TRACKTUNNEL03\n058=TRACKTUNNEL04\n059=RAILBRDG1\n060=RAILBRDG2\n061=CRAT01\n062=CRAT02\n063=CRAT03\n064=CRAT04\n065=CRAT0A\n066=CRAT0B\n067=CRAT0C\n068=DRUM01\n069=DRUM02\n070=PALET01\n071=PALET02\n072=PALET03\n073=PALET04\n074=LOBRDG01\n075=LOBRDG02\n076=LOBRDG03\n077=LOBRDG04\n078=LOBRDG05\n079=LOBRDG06\n080=LOBRDG07\n081=LOBRDG08\n082=LOBRDG09\n083=LOBRDG10\n084=LOBRDG11\n085=LOBRDG12\n086=LOBRDG13\n087=LOBRDG14\n088=LOBRDG15\n089=LOBRDG16\n090=LOBRDG17\n091=LOBRDG18\n092=LOBRDG19\n093=LOBRDG20\n094=LOBRDG21\n095=LOBRDG22\n096=LOBRDG23\n097=LOBRDG24\n098=LOBRDG25\n099=LOBRDG26\n100=LOBRDG27\n101=LOBRDG28\n102=TIB01\n103=TIB02\n104=TIB03\n105=TIB04\n106=TIB05\n107=TIB06\n108=TIB07\n109=TIB08\n110=TIB09\n111=TIB10\n112=TIB11\n113=TIB12\n114=TIB13\n115=TIB14\n116=TIB15\n117=TIB16\n118=TIB17\n119=TIB18\n120=TIB19\n121=TIB20\n122=LOBRDGE1\n123=LOBRDGE2\n124=LOBRDGE3\n125=LOBRDGE4\n126=VEINS\n127=TIB2_01\n128=TIB2_02\n129=TIB2_03\n130=TIB2_04\n131=TIB2_05\n132=TIB2_06\n133=TIB2_07\n134=TIB2_08\n135=TIB2_09\n136=TIB2_10\n137=TIB2_11\n138=TIB2_12\n139=TIB2_13\n140=TIB2_14\n141=TIB2_15\n142=TIB2_16\n143=TIB2_17\n144=TIB2_18\n145=TIB2_19\n146=TIB2_20\n147=TIB3_01\n148=TIB3_02\n149=TIB3_03\n150=TIB3_04\n151=TIB3_05\n152=TIB3_06\n153=TIB3_07\n154=TIB3_08\n155=TIB3_09\n156=TIB3_10\n157=TIB3_11\n158=TIB3_12\n159=TIB3_13\n160=TIB3_14\n161=TIB3_15\n162=TIB3_16\n163=TIB3_17\n164=TIB3_18\n165=TIB3_19\n166=TIB3_20\n167=VEINHOLE\n168=SROCK01\n169=SROCK02\n170=SROCK03\n171=SROCK04\n172=SROCK05\n173=TROCK01\n174=TROCK02\n175=TROCK03\n176=TROCK04\n177=TROCK05\n178=VEINHOLEDUMMY\n179=CRATE\n\n; These are graphic objects that can have an effect on the game.\n\n; Tiberium = Is this tiberium [Tiberium grown and graphic logic applies] (def=no)?\n; Crate = Is this overlay a crate (def=no)?\n; CrateTrigger = Is crate to trigger game events (def=no)?\n; RadarInvisible = Is this overlay not visible on the radar map (def=no)?\n; Explodes = Does it explode violently when destroyed [i.e., does it do collateral damage] (def=no)?\n; LegalTarget = Can it be a legal target for attack (def=no)?\n; ChainReaction = Does it explode and affect adjacent cells (def=no)?\n\n[TIB01]\nName=Tiberium (Green)\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB02]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB03]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB04]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB05]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB06]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB07]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB08]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB09]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB10]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB11]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB12]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB13]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB14]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB15]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB16]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB17]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB18]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB19]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB20]\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\n\n[TIB2_01]\nImage=TIB01\nName=Tiberium (Blue)\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_02]\nImage=TIB02\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_03]\nImage=TIB03\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_04]\nImage=TIB04\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_05]\nImage=TIB05\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_06]\nImage=TIB06\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_07]\nImage=TIB07\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_08]\nImage=TIB08\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_09]\nImage=TIB09\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_10]\nImage=TIB10\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_11]\nImage=TIB11\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_12]\nImage=TIB12\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_13]\nImage=TIB13\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_14]\nImage=TIB14\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_15]\nImage=TIB15\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_16]\nImage=TIB16\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_17]\nImage=TIB17\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_18]\nImage=TIB18\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_19]\nImage=TIB19\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB2_20]\nImage=TIB20\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_01]\nImage=TIB01\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_02]\nImage=TIB02\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_03]\nImage=TIB03\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_04]\nImage=TIB04\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_05]\nImage=TIB05\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_06]\nImage=TIB06\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_07]\nImage=TIB07\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_08]\nImage=TIB08\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_09]\nImage=TIB09\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_10]\nImage=TIB10\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_11]\nImage=TIB11\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_12]\nImage=TIB12\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_13]\nImage=TIB13\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_14]\nImage=TIB14\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_15]\nImage=TIB15\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_16]\nImage=TIB16\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_17]\nImage=TIB17\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_18]\nImage=TIB18\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_19]\nImage=TIB19\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[TIB3_20]\nImage=TIB20\nName=Tiberium\nTiberium=yes\nLegalTarget=false\nRadarInvisible=false\nChainReaction=yes\n\n[VEINHOLE]\nName=Veinhole Monster\nLegalTarget=yes\nRadarColor=92,92,0\nLand=Rock\nIsVeinholeMonster=true\nIsVeins=true\nNoUseTileLandType=true\n\n[VEINHOLEDUMMY]\nName=Veinhole Monster Dummy\nImage=blahblahblah\nLegalTarget=no\nRadarColor=92,92,0\nIsVeins=true\n\n[SROCK01]\nLegalTarget=false\nName=Sand Rock #1\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[SROCK02]\nLegalTarget=false\nName=Sand Rock #2\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[SROCK03]\nLegalTarget=false\nName=Sand Rock #3\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[SROCK04]\nLegalTarget=false\nName=Sand Rock #4\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[SROCK05]\nLegalTarget=false\nName=Sand Rock #5\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[TROCK01]\nLegalTarget=false\nName=Clear Rock #1\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false;\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[TROCK02]\nLegalTarget=false\nName=Clear Rock #2\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[TROCK03]\nLegalTarget=false\nName=Clear Rock #3\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[TROCK04]\nLegalTarget=false\nName=Clear Rock #4\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[TROCK05]\nLegalTarget=false\nName=Clear Rock #5\nLand=Rock\nRadarColor=64,64,64\nNoUseTileLandType=true\nDrawFlat=false\nIsARock=true ;WST 6/21/99 all pesky rocks need to have this\n\n[BTIB01]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB02]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB03]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB04]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB05]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB06]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB07]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB08]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB09]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB10]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB11]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[BTIB12]\nName=Large Tiberium\nTiberium=yes\nLegalTarget=false\nRadarColor=80,0,0\nLand=Rock\nChainReaction=yes\nCellAnim=BIGBLUE\nImage=None\nNoUseTileLandType=true\nDrawFlat=false\n\n[TRACKS01]\nName=Track NwSe\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS02]\nName=Track NeSw\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS03]\nName=Track NS\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS04]\nName=Track EW\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS05]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS06]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS07]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS08]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS09]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS10]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS11]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS12]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS13]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS14]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS15]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKS16]\nName=Train Tracks\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\nRadarInvisible = false\n\n[TRACKTUNNEL01]\nName=Train Tracks\nImage=TRTUNN01\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\n\n[TRACKTUNNEL02]\nName=Train Tracks\nImage=TRTUNN02\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\n\n[TRACKTUNNEL03]\nName=Train Tracks\nImage=TRTUNN03\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\n\n[TRACKTUNNEL04]\nName=Train Tracks\nImage=TRTUNN04\nLand=Railroad\nLegalTarget=false\nRadarColor=92,92,92\n\n[LOBRDG01]\nImage=LOBRDG01\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG02]\nImage=LOBRDG02\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG03]\nImage=LOBRDG03\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG04]\nImage=LOBRDG04\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG05]\nImage=LOBRDG05\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG06]\nImage=LOBRDG06\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG07]\nImage=LOBRDG07\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG08]\nImage=LOBRDG08\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG09]\nImage=LOBRDG09\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG10]\nImage=LOBRDG10\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG11]\nImage=LOBRDG11\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG12]\nImage=LOBRDG12\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG13]\nImage=LOBRDG13\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG14]\nImage=LOBRDG14\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG15]\nImage=LOBRDG15\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG16]\nImage=LOBRDG16\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG17]\nImage=LOBRDG17\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG18]\nImage=LOBRDG18\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG19]\nImage=LOBRDG19\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG20]\nImage=LOBRDG20\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG21]\nImage=LOBRDG21\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG22]\nImage=LOBRDG22\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG23]\nImage=LOBRDG23\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG24]\nImage=LOBRDG24\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG25]\nImage=LOBRDG25\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG26]\nImage=LOBRDG26\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=true\nRadarInvisible = false\n\n[LOBRDG27]\nImage=LOBRDG27\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=false\nRadarInvisible = true\n\n[LOBRDG28]\nImage=LOBRDG28\nName=Low Bridge\nLand=Road\nRadarColor=92,92,92\nNoUseTileLandType=false\nRadarInvisible = true\n\n[LOBRDGE1]\nImage=LOBRDGE1\nName=Low Bridge End 1\nLand=Road\nNoUseTileLandType=true\nRadarColor=92,92,92\n\n[LOBRDGE2]\nImage=LOBRDGE2\nName=Low Bridge End 2\nLand=Road\nNoUseTileLandType=true\nRadarColor=92,92,92\n\n[LOBRDGE3]\nImage=LOBRDGE3\nName=Low Bridge End 3\nLand=Road\nNoUseTileLandType=true\nRadarColor=92,92,92\n\n[LOBRDGE4]\nImage=LOBRDGE4\nName=Low Bridge End 4\nLand=Road\nNoUseTileLandType=true\nRadarColor=92,92,92\n\n[VEINS]\nImage=VEINS\nName=Tiberium Veins\nRadarColor=0,0,92\nIsVeins=true\nLand=Weeds\n\n[RAILBRDG1]\nImage=RAILBRDG\nName=Railroad Bridge 1\nLegalTarget=true\nRadarColor=92,92,92\nOverrides=yes\nNoUseTileLandType=false\n\n[RAILBRDG2]\nImage=RAILBRDG\nName=Railroad Bridge 2\nLegalTarget=true\nRadarColor=92,92,92\nOverrides=yes\nNoUseTileLandType=false\n\n[BRIDGE1]\nImage=BRIDGE\nName = Bridge 1\nLegalTarget=true\nRadarColor=92,92,92\nOverrides=yes\nNoUseTileLandType=false\n\n[BRIDGE2]\nImage=BRIDGE\nName = Bridge 2\nLegalTarget=true\nRadarColor=92,92,92\nOverrides=yes\nNoUseTileLandType=false\n\n[CRATE]\nName=Goodie Crate\nRadarColor=92,92,92\nCrate=yes\nCrateTrigger=yes\nRadarInvisible=yes\nLand=Clear\nDrawFlat=false\n\n[CRAT01]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\n;Crate=yes\n;CrateTrigger=yes\n;RadarInvisible=yes\nLand=Rock\n\n[CRAT02]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\n;Crate=yes\n;RadarInvisible=yes\nLand=Rock\n\n[CRAT03]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\n;Crate=yes\n;RadarInvisible=yes\nLand=Rock\n\n[CRAT04]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[CRAT0A]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[CRAT0B]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[CRAT0C]\nName=Crate\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[DRUM01]\nName=Drum\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[DRUM02]\nName=Drum\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[PALET01]\nName=Palette\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[PALET02]\nName=Palette\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[PALET03]\nName=Palette\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n[PALET04]\nName=Palette\nLegalTarget=true\nRadarColor=92,92,92\nLand=Rock\n\n\n\n; *** Smudge Objects ***\n; This is the list of smudge objects. Typically, these include\n; craters and scorch marks.\n[SmudgeTypes]\n1=CR1\n2=CR2\n3=CR3\n4=CR4\n5=CR5\n6=CR6\n7=BURN01\n8=BURN02\n9=BURN03\n10=BURN04\n11=BURN05\n12=BURN06\n13=BURN07\n14=BURN08\n15=BURN09\n16=BURN10\n17=BURN11\n18=BURN12\n19=BURN13\n20=BURN14\n21=BURN15\n22=BURN16\n23=BURNT01\n24=BURNT02\n25=BURNT03\n26=BURNT04\n27=BURNT05\n28=BURNT06\n29=BURNT07\n30=BURNT08\n31=BURNT09\n32=BURNT10\n33=BURNT11\n34=BURNT12\n35=CRATER01\n36=CRATER02\n37=CRATER03\n38=CRATER04\n39=CRATER05\n40=CRATER06\n41=CRATER07\n42=CRATER08\n43=CRATER09\n44=CRATER10\n45=CRATER11\n46=CRATER12\n\n; These specify the objects (actually more of an artwork\n; element than a game object) called smudges. These typically\n; are used to mark the effects of battle.\n\n; Crater = Is this a crater smudge [special growth logic] (def=no)?\n[CRATER01]\nCrater=yes\n\n[CRATER02]\nCrater=yes\n\n[CRATER03]\nCrater=yes\n\n[CRATER04]\nCrater=yes\n\n[CRATER05]\nCrater=yes\n\n[CRATER06]\nCrater=yes\n\n[CRATER07]\nCrater=yes\n\n[CRATER08]\nCrater=yes\n\n[CRATER09]\nCrater=yes\n\n[CRATER10]\nCrater=yes\n\n[CRATER11]\nCrater=yes\nWidth=2\nHeight=2\n\n[CRATER12]\nCrater=yes\nWidth=2\nHeight=2\n\n[BURNT01]\nBurn=yes\n\n[BURNT02]\nBurn=yes\n\n[BURNT03]\nBurn=yes\n\n[BURNT04]\nBurn=yes\n\n[BURNT05]\nBurn=yes\n\n[BURNT06]\nBurn=yes\n\n[BURNT07]\nBurn=yes\nWidth=2\n\n[BURNT08]\nBurn=yes\nWidth=2\n\n[BURNT09]\nBurn=yes\nHeight=2\n\n[BURNT10]\nBurn=yes\nHeight=2\n\n[BURNT11]\nBurn=yes\nWidth=2\nHeight=2\n\n[BURNT12]\nBurn=yes\nWidth=2\nHeight=2\n\n[CR1]\n\n[CR2]\n\n[CR3]\n\n[CR4]\n\n[CR5]\n\n[CR6]\n\n[BURN01]\n\n[BURN02]\n\n[BURN03]\n\n[BURN04]\n\n[BURN05]\n\n[BURN06]\n\n[BURN07]\n\n[BURN08]\n\n[BURN09]\n\n[BURN10]\n\n[BURN11]\n\n[BURN12]\n\n[BURN13]\n\n[BURN14]\n\n[BURN15]\n\n[BURN16]\n\n\n; ******* Land Characteristics *******\n; This section specifies the characteristics of the various\n; terrain types. The primary purpose is to differentiate the\n; movement capabilities.\n\n; Float = % of full speed for ships [0 means impassable] (def=100)\n; Foot = % of full speed for foot soldiers [0 means impassable] (def=100)\n; Track = % of full speed for tracked vehicles [0 means impassable] (def=100)\n; Wheel = % of full speed for wheeled vehicles [0 means impassable] (def=100)\n; Hover = % of full speed for hovering vehicles [0 means impassable] (def=100)\n; Amphibious = % of full speed for amphibious vehicles [0 impassable] (def=100)\n; Buildable = Can buildings be built upon this terrain (def=no)?\n\n; clear grassy terrain\n[Clear]\nFoot=90%\nTrack=70%\nWheel=70%\nFloat=0%\nHover=100%\nAmphibious=80%\nCreep=100%\nBuildable=yes\n\n; rocky terrain\n[Rough]\nFoot=80%\nTrack=60%\nWheel=40%\nFloat=0%\nHover=100%\nAmphibious=40%\nCreep=90%\nBuildable=yes\n\n; roads\n[Road]\nFoot=100%\nTrack=100%\nWheel=100%\nHover=100%\nFloat=0%\nAmphibious=100%\nCreep=100%\nBuildable=yes\n\n; open water\n[Water]\nFoot=0%\nTrack=0%\nWheel=0%\nHover=100%\nFloat=100%\nAmphibious=80%\nCreep=0%\nBuildable=no\n\n; cliffs\n[Rock]\nFoot=0%\nTrack=0%\nWheel=0%\nFloat=0%\nHover=0%\nAmphibious=0%\nCreep=0%\nBuildable=no\n\n; walls and other man made obstacles\n[Wall]\nFoot=0%\nTrack=0%\nWheel=0%\nFloat=0%\nHover=0%\nAmphibious=0%\nCreep=0%\nBuildable=no\n\n; Tiberium\n[Tiberium]\nFoot=90%\nTrack=70%\nWheel=50%\nFloat=0%\nHover=100%\nAmphibious=50%\nCreep=100%\nBuildable=no\n\n; Vein hole creater weeds\n[Weeds]\nFoot=50%\nTrack=70%\nWheel=50%\nFloat=0%\nHover=100%\nAmphibious=50%\nCreep=90%\nBuildable=no\n\n; sandy beach\n[Beach]\nFoot=0%\nTrack=0%\nWheel=0%\nFloat=0%\nHover=100%\nAmphibious=60%\nCreep=0%\nBuildable=no\n\n; ice\n[Ice]\nFoot=50%\nTrack=80%\nWheel=50%\nFloat=0%\nHover=100%\nAmphibious=50%\nCreep=100%\nBuildable=no\n\n; train tracks\n[Railroad]\nFoot=90%\nTrack=100%\nWheel=50%\nFloat=0%\nHover=100%\nAmphibious=50%\nCreep=100%\nBuildable=no\n\n; tunnels\n[Tunnel]\nFoot=100%\nTrack=100%\nWheel=100%\nFloat=0%\nHover=100%\nAmphibious=100%\nCreep=100%\nBuildable=no\n\n\n; ******* Random Crate Powerups *******\n; This specifies the chance for the specified crate powerup to appear\n; in a 'random' crate. The chance is expressed in the form of 'shares'\n; out of the total shares specified. The second parameter is the animation\n; to use when this crate is picked up. The third parameter, if present, specifies\n; the data value needed for that crate powerup. They mean different things\n; for the different powerups.\n[Powerups]\nArmor=33,ARMOR,0.5\t\t\t\t; armor of nearby objects increased (armor multiplier)\nCloak=20,CLOAK\t\t\t\t\t; enable cloaking on nearby objects\nDarkness=5,SHROUDX\t\t\t\t; cloak entire radar map\nExplosion=38,<none>,500\t\t\t; high explosive baddie (damage per explosion)\nFirepower=28,FIREPOWR,2.0\t\t; firepower of nearby objects increased (firepower multiplier)\nHealBase=23,HEALALL\t\t\t\t; all buildings to full strength\nICBM=13,MLTIMISL\t\t\t\t; nuke missile one time shot (was CHEMISLE)\nMoney=55,MONEY,2000\t\t\t\t; a chunk o' cash (maximum cash)\nNapalm=25,<none>,600\t\t\t; fire explosion baddie (damage)\nReveal=8,REVEAL\t\t\t\t\t; reveal entire radar map\nSpeed=30,ARMOR,1.7\t\t\t\t; speed of nearby objects increased (speed multiplier)\nSquad=45,<none>\t\t\t\t\t; squad of random infantry\nUnit=40,<none>\t\t\t\t\t; vehicle\nInvulnerability=10,ARMOR,1.0\t; invulnerability (duration in minutes)\nVeteran=15,VETERAN,1\t\t\t; veteran upgrade (levels to upgrade)\nIonStorm=0,<none>\t\t\t\t; initiate ion storm\nGas=18,<none>,100\t\t\t\t; tiberium gas (damage for each gas cloud)\nTiberium=35,<none>\t\t\t\t; tiberium patch\nPod=0,<none>\t\t\t\t\t; drop pod special\n\n; ******* Tiberium Varieties *******\n; There are various kinds of tiberium. This lists their number and\n; particulars.\n\n[Tiberiums]\n0=Riparius\n1=Cruentus\n2=Vinifera\n3=Aboreus\n\n; Name = display name\n; Image = image to use [1=small, 2=large, 3=vine]\n; Value = credit value per 'bail'\n; Growth = growth rate\n; Spread = spread rate\n; Power = explosive power per 'bail' (def=0)\n; Color = display color of the Tiberium\n; Shard = crystal to fly off when chain reacting (def=none)\n\n; This is green tiberium.  It grows and spreads quickly and is not explosive\n[Riparius]\nName=Tiberium Riparius\nImage=1\nPower=1 ;4\nValue=25\nGrowth=2200\nGrowthPercentage=.09\nSpread=2200\nSpreadPercentage=.09\nColor=NeonGreen\t\t\t; **WARNING**: If you change this color, notify Bret_a\n\n; This is the big tiberium crystal.  It does not grow or spread, is impassable and is explosive\n; Not currently in use in TS (AI)\n[Cruentus]\nName=Tiberium Cruentus\nImage=2\nValue=70\nGrowth=10000\nGrowthPercentage=0\nSpread=10000\nSpreadPercentage=0\nPower=1\t;10\nColor=NeonBlue\t\t\t; **WARNING**: If you change this color, notify Bret_a\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n; This is blue tiberium.  It grows and spreads slowly and is explosive.\n[Vinifera]\nName=Tiberium Vinifera\nImage=3\nValue=40\nGrowth=10000\nGrowthPercentage=.05\nSpread=10000\nSpreadPercentage=.05\nPower=20\t;100\nColor=NeonBlue\t\t; **WARNING**: If you change this color, notify Bret_a\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n; This is blue tiberium.  It grows and spreads slowly and is explosive.  This entry should be\n; the same as [vinifera] except for Name and Image\n[Aboreus]\nName=Tiberium Aboreus\nImage=4\nValue=30\nGrowth=10000\nGrowthPercentage=.05\nSpread=10000\nSpreadPercentage=.05\nPower=1\t;10\nColor=NeonBlue\t\t; **WARNING**: If you change this color, notify Bret_a\nDebris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4\n\n\n; ******* Mission Control *******\n; This specifies the various general behavior characteristics of\n; the missions that objects can be assigned. Each of the game objects must\n; be in a mission. The mission behavior is generally hard coded into\n; the program, but there are some behavior characteristics that can\n; be overridden. Don't modify these.\n\n; NoThreat = Is its weapons disabled and thus ignored as a potential target until fired upon (def=no)?\n; Zombie = Is forced to sit there like a zombie and never recovers (def=no)?\n; Recruitable = Can it be recruited into a team or base defense (def=yes)?\n; Paralyzed = Is the object frozen in place but can still fire and function (def=no)?\n; Retaliate = Is allowed to retaliate while on this mission (def=yes)?\n; Scatter = Is allowed to scatter from threats (def=yes)?\n; Rate = delay between normal processing (larger = faster game, less responsiveness)\n; AARate = anti-aircraft delay rate (if not specifed it uses regular rate).\n\n; Unit sits still and plays dead.\n[Sleep]\nRecruitable=no\nZombie=yes\nRetaliate=no\nScatter=no\nRate=1\n\n; Unit doesn't fire and is not considered a threat.\n[Harmless]\nRecruitable=no\nNoThreat=yes\nRetaliate=no\nRate=.5\n\n; Just like guard mode, but cannot move.\n[Sticky]\nRecruitable=no\nParalyzed=yes\nScatter=no\nRate=.016\n\n; Special attack mission used by team logic.\n[Attack]\nRate=.016\nAARate=.016\n\n; Move to destination.\n[Move]\nRate=.016\n\n; Patrol a series of waypoints\n[Patrol]\nRate=.016\n\n; Special move to destination after all other queued moves occur.\n[QMove]\nRate=.016\n\n; Run away (possibly leave the map).\n[Retreat]\nRecruitable=no\nRetaliate=no\nRate=.1\n\n; Sit around and engage any enemy that wanders within weapon range.\n[Guard]\nRate=.030\nAARate=.016\n\n; Enter building or transport for loading purposes.\n[Enter]\nRetaliate=no\nRecruitable=no\nRate=.016\n\n; Engineer entry logic.\n[Capture]\nRetaliate=no\nRecruitable=no\nScatter=no\nRate=.016\n\n; Handle harvest ore - dump at refinery loop.\n[Harvest]\nRetaliate=no\nRecruitable=no\nScatter=no\nRate=.016\n\n; Guard the general area where the unit starts at.\n[Area Guard]\nRecruitable=yes\nRate=.040\nAARate=.032\n\n; <unused>\n[Return]\n\n; Stop moving and firing at the first available opportunity.\n[Stop]\n\n; <unused>\n[Ambush]\n\n; Scan for and attack any enemies whereever they may be.\n[Hunt]\nRecruitable=no\nRetaliate=no\nRate=.016\n\n; While dropping off cargo (e.g., APC unloading passengers).\n[Unload]\nRecruitable=no\nRetaliate=no\nScatter=no\nRate=.016\n\n; Tanya running to place bomb in building.\n[Sabotage]\nRecruitable=no\nRate=.016\n\n; Buildings use this when building up after initial placement.\n[Construction]\nRecruitable=no\nRetaliate=no\nScatter=no\n\n; Buildings use this when deconstruction after being sold.\n[Selling]\nRecruitable=no\nNoThreat=yes\nRetaliate=no\nScatter=no\n\n; Service depot uses this mission to repair attached object.\n[Repair]\nRate=.08\n\n; Special team override mission.\n[Rescue]\nRate=.016\n\n; Missile silo special launch missile mission.\n[Missile]\nRate=.1\n\n; While opening or closing a gate to allow passage.\n[Open]\nRate=.016\n\n\n; *** Voxel Animation List ***\n; This is the complete list of voxel animations available.\n; VoxelAnims are meant to be flying debris.  Things like\n; turrets and tires make good voxel anims.\n[VoxelAnims]\n1=PIECE\n2=TIRE\n3=GASTANK\n4=SONICTURRET\n5=4TNKTURRET\n6=CRYSTAL01\n7=CRYSTAL02\n8=METEOR01\n9=METEOR02\n10=PEBBLE\n\n; ******* Voxel Debris types *******\n; Translucent = is the debris to be drawn with translucency (def=no)?\n; Elasticity = \"bounciness\" of the object [should be 0.0 through 1.0] (def=0.75)\n; MinAngularVelocity = minimum rate at which the debris is to spin in degrees (def=0.0)\n; MaxAngularVelocity = maximum rate at which the debris is to spin in degrees (def=10.0)\n; Duration = max number of frames to let the debris exist (def=30)\n; MinZVel = minimum starting Z velocity (def=3.5)\n; MaxZVel = maximum starting Z velocity (def=5.0)\n; MaxXYVel = maximim starting lateral velocity (def=15.0)\n; ShareBodyData = Get the voxel data from another Type's body voxel data (def = no)?\n; ShareTurretData = Get the voxel data from another Type's turret voxel data (def = no)?\n; ShareBarrelData = Get the voxel data from another Type's barrel voxel data (def = no)?\n; ShareSource = name of the object to share voxel data from (def = none)\n; VoxelIndex = voxel piece within the voxel data to use (def = 0)\n; StartSound = sound to play when the voxel anim is created (def = VOC_NONE).\n; BounceSound = sound to play when the voxel anim bounces (def = VOC_NONE).\n; ExpireSound = sound to play when the voxel anim expires (def = VOC_NONE).\n; BounceAnim = animation to launch when the voxel anim bounces (def = ANIM_NONE).\n; ExpireAnim = animation to launch when the voxel anim expires (def = ANIM_NONE).\n; TrailerAnim = animation to trail behind the object (usually smoke or flame)\n; DamageRadius = the debris damages objects that it hits if they're closer than this distance\n; Damage = amount of damage to apply to objects that are hit\n; Warhead = warhead to use for damage purposes\n; AttachedSystem = particle system to attach to the voxel anim\n; Spawns = the particle spawned when this voxel debris explodes (def = none)\n; SpawnCount = number of particles spawned [on average] (def = 0)\n\n[PIECE]\nName=Scrap Metal Debris\nElasticity=0\nMinAngularVelocity=5.0\nMaxAngularVelocity=9.0\nMinZVel=24.0\nMaxZVel=28.0\nMaxXYVel=15.0\nDuration=75\nDamage=5\nExpireAnim=TWLT036\nDamageRadius=100\nWarhead=TankOGas\n\n[TIRE]\nName=Flying Tire\nElasticity=0.8\nMinAngularVelocity=12.0\nMaxAngularVelocity=24.0\nMinZVel=28.0\nMaxZVel=32.0\nMaxXYVel=10.0\nDuration=150\n\n[GASTANK]\nName=Flying Gas Tank\nElasticity=0.0\nMinAngularVelocity=9.0\nMaxAngularVelocity=15.0\nMinZVel=30.0\nMaxZVel=35.0\nMaxXYVel=8.0\nDuration=100\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=100\nWarhead=TankOGas\n\n[SONICTURRET]\nName=Disruptor Turret\nShareTurretData=yes\nShareSource=SONIC\nElasticity=0.0\nMinAngularVelocity=10.0\nMaxAngularVelocity=14.0\nMinZVel=30.0\nMaxZVel=38.0\nMaxXYVel=8.0\nDuration=100\nExpireAnim=TWLT026\nDamage=90\nDamageRadius=100\nWarhead=TankOGas\n\n[4TNKTURRET]\nName=Mammoth Tank Turret\nShareTurretData=yes\nShareSource=4TNK\nElasticity=0.0\nMinAngularVelocity=10.0\nMaxAngularVelocity=14.0\nMinZVel=30.0\nMaxZVel=38.0\nMaxXYVel=8.0\nDuration=100\nExpireAnim=TWLT036\nDamage=30\nDamageRadius=50\nWarhead=TankOGas\n\n[CRYSTAL01]\nName=TiberiumCrystal01\nShareTurretData=yes\nShareSource=SONIC\nElasticity=0.0\nMinAngularVelocity=12.0\nMaxAngularVelocity=24.0\nMinZVel=28.0\nMaxZVel=32.0\nMaxXYVel=10.0\nDuration=150\nExpireAnim=TWLT050\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\n\n[CRYSTAL02]\nName=TiberiumCrystal02\nImage=GASTANK\nElasticity=0.0\nMinAngularVelocity=12.0\nMaxAngularVelocity=24.0\nMinZVel=40.0\nMaxZVel=45.0\nMaxXYVel=18.0\nDuration=150\nExpireAnim=TWLT050\nDamage=40\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\n\n[METEOR01]\nName=Meteorite01\nImage=MTRS\nElasticity=0.0\nMinAngularVelocity=12.0\nMaxAngularVelocity=30.0\nMinZVel=-100.0\nMaxZVel=-100.0\nMaxXYVel=100.0\nDuration=70\nExpireAnim=TWLT070\nDamage=500\nDamageRadius=300\nWarhead=Meteorite\nIsMeteor=true\nSpawns=PEBBLE\nSpawnCount=5\n\n[METEOR02]\nName=Meteorite02\nImage=MTRB\nElasticity=0.0\nMinAngularVelocity=12.0\nMaxAngularVelocity=30.0\nMinZVel=-100.0\nMaxZVel=-100.0\nMaxXYVel=100.0\nDuration=70\nExpireAnim=TWLT100\nDamage=500\nDamageRadius=300\nWarhead=Meteorite\nIsMeteor=true\nIsTiberium=true\nSpawns=PEBBLE\nSpawnCount=7\n\n[PEBBLE]\nName=TiberiumShard\nImage=MTRT\nElasticity=0.0\nMinAngularVelocity=12.0\nMaxAngularVelocity=24.0\nMinZVel=40.0\nMaxZVel=45.0\nMaxXYVel=18.0\nDuration=150\nExpireAnim=TWLT036\nDamage=20\nDamageRadius=100\nWarhead=TankOGas\nIsTiberium=true\n\n\n; ******* Special Weapon types *******\n; This is a list of the various types of super weapons available in the game\n[SuperWeaponTypes]\n1=MultiSpecial\n2=EMPulseSpecial\n3=FirestormSpecial\n4=IonCannonSpecial\n5=HuntSeekSpecial\n6=ChemicalSpecial\n7=DropPodSpecial\n\n; IsPowered -- does this super weapon become inoperative in a low power situation?\n; RechargeVoice -- Voice to use when weapon is fully recharged and ready.\n; ChargingVoice -- Voice to use when weapon begins charging.\n; ImpatientVoice -- Voice to use when user clicks on weapon that isn't finished charging.\n; SuspendVoice -- Voice to use when special weapon charging is suspended.\n; RechargeTime -- time in minutes to recharge this special\n\n; Chem weapon. The logic will fail if this weapon is 'powered'\n[ChemicalSpecial]\nName=Chemical Missile\nIsPowered=false\nRechargeVoice=00-I152\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=.3\nType=ChemMissile\nSidebarImage=ChemIcon\nAction=ChemBomb\nManualControl=yes\nWeaponType=ChemLauncher\nAuxBuilding=NAWAST\n\n[MultiSpecial]\nName=Multi-Missile\nIsPowered=true\nRechargeVoice=00-I154\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=10\nType=MultiMissile\nSidebarImage=MltiIcon\nAction=Nuke\nWeaponType=MultiLauncher\n\n[EMPulseSpecial]\nName=E.M. Pulse\nIsPowered=true\nRechargeVoice=00-I158\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=4.5\nType=EMPulse\nSidebarImage=PulsIcon\nAction=EMPulse\n\n[FirestormSpecial]\nName=Firestorm Defense\nIsPowered=true\nRechargeVoice=00-I162\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=3\nType=Firestorm\nSidebarImage=FSTDICON\nUseChargeDrain=true\n\n[IonCannonSpecial]\nName=Ion Cannon\nIsPowered=true\nRechargeVoice=00-I156\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=8.5\nType=IonCannon\nAction=IonCannon\nSidebarImage=IONCICON\n\n[HuntSeekSpecial]\nName=Hunter Seeker\nIsPowered=true\nRechargeVoice=\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=12\nType=HunterSeeker\nSidebarImage=DETNICON\n\n[DropPodSpecial]\nName=Drop Pod\nIsPowered=true\nRechargeVoice=00-I506\nChargingVoice=\nImpatientVoice=\nSuspendVoice=\nRechargeTime=7  ;was 6\nType=DropPod\nAction=DropPod\nSidebarImage=PODSICON\n\n\n; ******* Globals Variable Names *******\n; These must be constant throughout all scenarios based on these\n; rules. These are numbered starting from zero. Do not change the\n; number values or else all preexisting triggers using them will\n; break.\n\n[VariableNames]\n0=<Alternate Start Location>\n1=<Alternate Next Scenario>\n2=<reserved2>\n3=Sensor Array Down\n4=Toxin Trucks Found\n5=Dam Destroyed\n6=Ion Cannon Codes Found\n7=C4 Placed\n8=Completed 3B\n9=Prisoners Freed\n10=Train Stolen\n11=Completed 9B\n12=Difficulty Easy\n13=Difficulty Medium\n14=Difficulty Hard\n15=Difficulty Easy/Medium\n16=Difficulty Medium/Hard\n"
  },
  {
    "path": "DXMainClient/Resources/INI/snow.ini",
    "content": ";Modified July 30, 2002\n;Marble madness set added\n;By DJBREIT\n; Version 1.0\n;\n;           ***Tiberian Sun Isometric Tile Control File***\n;\n\n\n;\n; General section.\n;\n; RampBase\n;  Number of tile set that includes all the ramp types\n;\n; MMRampBase\n;  Number of tile set that has the marble madness mode ramps\n;\n; ClearTile\n;  Number of tile set to use for clear terrain\n;\n; RoughTile\n;  Number of tile set that has the rough terrain\n;\n; ClearToRoughLAT\n;  Tile set that has the 16 tiles for the clear/rough LAT system\n;\n; HeightBase\n;  First tile of marble madness height tiles\n;\n; BlackTile\n;  Black tile used when rendering non-existent cells\n;\n; BridgeSet\n;  Tile set that contains bridge edges\n;\n; BridgeTopLeft1\n; BridgeTopLeft2\n; BridgeBottomRight1\n; BridgeBottomRight2\n; BridgeTopRight1\n; BridgeTopRight2\n; BridgeBottomLeft1\n; BridgeBottomLeft2\n;  Tiles in bridge set to search for when fixing up bridges\n;\n;\n\n[General]\nPaveTile = 68\nMiscPaveTile = 69\nClearToPaveLat = 70\nRampBase = 9\nMMRampBase = 7\nRampSmooth = 41\nClearTile = 0\nRoughTile = 13\nClearToRoughLat = 14\nHeightBase = 46\nCliffSet = 10\nShorePieces = 12\nWaterSet = 21\nIce1Set = 31\nIce2Set = 32\nIce3Set = 33\nIceShoreSet = 34\nBlackTile = 6\nBridgeSet = 19\nTrainBridgeSet = 39\nSlopeSetPieces = 25\nSlopeSetPieces2 = 26\nMonorailSlopes = 45\nTunnels=47\nTrackTunnels = 49\nDirtTunnels = 66\nDirtTrackTunnels = 67\nWaterfallEast = 35\nWaterfallWest = 37\nWaterfallNorth = 36\nWaterfallSouth = 30\nCliffRamps = 25\nPavedRoads = 20\nPavedRoadEnds = 38\nDirtRoadJunction = 17\nDirtRoadCurve = 16\nDirtRoadStraight = 18\nRoughGround = 40\nWaterCliffs = 15\nDirtRoadSlopes = 23\nDestroyableCliffs = 61\nSandTile = 62\nClearToSandLat = 63\nGreenTile = 64\nClearToGreenLat = 65\nRocks=62\n\n\nBridgeTopLeft1 = 1\nBridgeTopLeft2 = 2\nBridgeBottomRight1 = 3\nBridgeBottomRight2 = 3\nBridgeTopRight1 = 4\nBridgeTopRight2 = 5\nBridgeBottomLeft1 = 6\nBridgeBottomLeft2 = 6\nBridgeMiddle1 = 7\nBridgeMiddle2 = 12\n\n\n\n\n;\n; TS Will scan through this file when loading up a theater and read in the\n; isometric tile files specified.\n;\n; [TileSetnnnn]\n;  This is the tile set section header. TS will loop through from TileSet0000\n;  upwards until it finds a set that hasnt been specified.\n;\n; SetName\n;  The name of the set as it will appear in the editor.\n;\n; FileName\n;  The base file name of each file in the set. The files in a set must all\n;  have the same basic name with a 2 digit id number appended. For example\n;  cliff01.tem, cliff02.tem, cliff03.tem. The 2 digit number starts at 01\n;  not 00.\n;\n; TilesInSet\n;  The number of files comprising the set. There is a practical limit of\n;  99 due to the 2 digit file name suffix.\n;\n; LastTilesInSet\n;  The number of tiles which the set used to have. This tells the\n;  game that the number of tiles in the set has changed and it should fix up\n;  the tile numbers when a map is loaded. If the map is then saved again,\n;  it will be saved with the correct tile numbers. This value should only\n;  be used to load up maps, convert the tile numbers, then save the maps\n;  out again. Then the LastTilesInSet entry should be removed or the newly\n;  fixed up maps will not load correctly.\n;\n; MarbleMadness\n;  The section number of the tile set to use for these tiles when in\n;  marble madness mode.\n;\n; NonMarbleMadness\n;  For marble madness tiles, this is the tile set to use when not in\n;  marble madness mode.\n;\n; Morphable\n;  Can this tile set be modified using the raise/lower ground function?\n;\n; ShadowCaster\n;  Do the tiles in this set cast shadows (cliff pieces)\n;\n; ToTemperateTheater\n;  The equivilent tile section in the temperate theater\n;\n; ToSnowTheater\n;  The equivilent tile section in the snow theater\n;\n; LowRadarColor\n;\tWhat color to show on the radar for this set at the lowest height\n;\n; HighRadarColor\n;\tWhat color to show on the radar for this set at the highest height\n\n\n;\n; Blank tile for filling in holes.\n;\n[TileSet0000]\nSetName = Clear\nFileName = Clear\nTilesInSet = 1\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; A few buildings\n;\n[TileSet0001]\nSetName = Misc Buildings\nFileName = Bld\nTilesInSet = 3\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowBurrowing=false\n\n;\n; Some basic flat tiles\n;\n[TileSet0002]\nSetName = Clear\nFileName = Snow\nTilesInSet = 4\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; A couple of old cliff pieces (not used)\n;\n[TileSet0003]\nSetName = Cliff Pieces\nFileName = clif\nTilesInSet = 2\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowBurrowing=false\n\n;\n; A large ice flow.\n;\n[TileSet0004]\nSetName = Ice Flow\nFileName = flow\nTilesInSet = 1\nLowRadarColor=192,192,192\nHighRadarColor=255,255,255\nAllowBurrowing=false\n\n;\n; A nice little house.\n;\n[TileSet0005]\nSetName = House\nFileName = house\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; Blank tile used for filling areas with no cell data.\n;\n[TileSet0006]\nSetName = Blank\nFileName = blank\nTilesInSet = 1\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Marble madness mode ramp pieces.\n;\n[TileSet0007]\nSetName = MM Ramps\nFileName = mslop\nTilesInSet = 20\nNonMarbleMadness = 9\nMorphable = true\n;LastTilesInSet = 16\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\n\n;\n; Height pieces for marble madness mode.\n;\n; Obsolete. Replaced with HITE01 - HITE10\n;\n[TileSet0008]\nSetName = Height Pieces\nFileName = mslop\nTilesInSet = 7\nMorphable = true\nAllowTiberium = true\n\n;\n; Misc theater ramps\n;\n[TileSet0009]\nSetName = Ice Ramps\nFileName = slope\nTilesInSet = 20\nMarbleMadness = 7\nMorphable = true\n;LastTilesInSet = 16\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Cliff set.\n;\n[TileSet0010]\nSetName = Cliff Set\nFileName = Cliff\nTilesInSet = 40\nMarbleMadness = 22\nShadowCaster = true\nShadowTiles = 40\nLowRadarColor=110,110,150\nHighRadarColor=150,150,190\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Civilian buildings\n;\n[TileSet0011]\nSetName = Civilian Buildings\nFileName = Civ\nTilesInSet = 8\nAllowBurrowing=false\n\n;\n; Shore pieces\n;\n[TileSet0012]\nSetName = Shore Pieces\nFileName = Shore\nTilesInSet = 42\nLowRadarColor=80,80,150\nHighRadarColor=80,80,150\nMarbleMadness=53\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Clear terrain (slightly rough)\n;\n[TileSet0013]\nSetName = Rough lat\nFileName = Ruff\nTilesInSet = 1\nMorphable = true\nLowRadarColor=130,130,192\nHighRadarColor=130,130,192\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; L.A.T. system for connecting clear and rough clear terrain\n;\n[TileSet0014]\nSetName = Clear/Rough LAT\nFileName = clat\nTilesInSet = 16\nMorphable = true\nLowRadarColor=140,140,192\nHighRadarColor=140,140,192\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Cliff pieces that meet water pieces\n;\n[TileSet0015]\nSetName = Cliff/Water pieces\nFileName = WCliff\nTilesInSet = 28\nShadowCaster = true\nShadowTiles = 28\nLowRadarColor=70,70,120\nHighRadarColor=90,90,200\nMarbleMadness=58\nAllowBurrowing=false\n\n;\n; Dirt roads. Corner pieces.\n;\n[TileSet0016]\nSetName = Bendy Dirt Roads\nFileName = Droadc\nTilesInSet = 24\nLowRadarColor=110,80,0\nHighRadarColor=130,90,0\nMarbleMadness=50\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Dirt roads. Junctions.\n;\n[TileSet0017]\nSetName = Dirt Road Junctions\nFileName = Droadj\nTilesInSet = 11\nLowRadarColor=110,80,0\nHighRadarColor=130,90,0\nMarbleMadness=51\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Dirt roads. Straights.\n;\n[TileSet0018]\nSetName = Straight Dirt Roads\nFileName = Droads\nTilesInSet = 66\nLowRadarColor=110,80,0\nHighRadarColor=130,90,0\nMarbleMadness=52\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Bridge sections.\n;\n[TileSet0019]\nSetName = Bridges\nFileName = Ovrps\nTilesInSet = 16\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\n\n;\n; Paved roads.\n;\n[TileSet0020]\nSetName = Paved Roads\nFileName = Proad\nTilesInSet = 21\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Just icy water.\n;\n[TileSet0021]\nSetName = Water\nFileName = Water\nTilesInSet = 14\nLowRadarColor=10,10,80\nHighRadarColor=15,15,110\nMarbleMadness=60\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Cliff set.\n;\n[TileSet0022]\nSetName = Marble Madness Cliff Set\nFileName = Mclif\nTilesInSet = 40\nNonMarbleMadness = 10\nShadowCaster = true\nShadowTiles = 40\nLowRadarColor=110,110,150\nHighRadarColor=150,150,190\nAllowBurrowing=false\n\n;\n; Dirt road slopes\n;\n[TileSet0023]\nSetName = Dirt Road Slopes\nFileName = DRSLPE\nTilesInSet = 8\nMarbleMadness = 24\nLowRadarColor=110,80,0\nHighRadarColor=130,90,0\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Marble Madness dirt road slopes\n;\n[TileSet0024]\nSetName = MM Dirt Road Slopes\nFileName = MDRSLP\nTilesInSet = 8\nNonMarbleMadness = 23\nLowRadarColor=110,80,0\nHighRadarColor=130,90,0\nAllowTiberium = true\n\n;\n; Slope set pieces\n;\n[TileSet0025]\nSetName = Slope Set Pieces\nFileName = RAMP\nTilesInSet = 10\nMarbleMadness = 26\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nShadowCaster = true\nShadowTiles = 10\nRequiredForRMG = true\n\n;\n; Slope set pieces - Marble Madness version\n;\n[TileSet0026]\nSetName = Slope Set Pieces\nFileName = MRAM\nTilesInSet = 10\nNonMarbleMadness = 25\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\n\n;\n; A dead oil tanker\n;\n[TileSet0027]\nSetName = Dead Oil Tanker\nFileName = TANKER\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; Some ruins\n;\n[TileSet0028]\nSetName = Ruins\nFileName = RUIN\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; Height pieces for marble madness mode\n; Replaced with 15 variation version.\n;\n[TileSet0029]\nSetName = Obsolete Height Pieces\nFileName = hyte\nTilesInSet = 10\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowToPlace=no\nAllowTiberium = true\n\n;\n; Waterfalls.\n;\n[TileSet0030]\nSetName = Waterfalls\nFileName = W-a-\nTilesInSet = 4\nMarbleMadness=54\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0031]\nSetName = Ice 01\nFileName = Ice01\nTilesInSet = 64\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\nMarbleMadness=31\n\n[TileSet0032]\nSetName = Ice 02\nFileName = Ice02\nTilesInSet = 64\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\nMarbleMadness=32\n\n[TileSet0033]\nSetName = Ice 03\nFileName = Ice03\nTilesInSet = 64\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\nMarbleMadness=33\n\n[TileSet0034]\nSetName = Ice shore\nFileName = Ishore\nTilesInSet = 48\nLowRadarColor=200,200,230\nHighRadarColor=200,200,230\nAllowBurrowing=false\nRequiredForRMG = true\nMarbleMadness=34\n\n[TileSet0035]\nSetName = Waterfalls-B\nFileName = W-b-\nTilesInSet = 4\nToTemperateTheater=49\nMarbleMadness=55\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0036]\nSetName = Waterfalls-C\nFileName = W-c-\nTilesInSet = 4\nToTemperateTheater=50\nMarbleMadness=56\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0037]\nSetName = Waterfalls-D\nFileName = W-d-\nTilesInSet = 4\nToTemperateTheater=51\nMarbleMadness=57\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0038]\nSetName = Paved Road Ends\nFileName = p_end\nTilesInSet = 4\nToTemperateTheater = 36\nMorphable = false\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Train Bridge sections.\n;\n[TileSet0039]\nSetName = TrainBridges\nFileName = Tovrps\nTilesInSet = 16\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\n\n\n[TileSet0040]\nSetName = Rough ground\nFileName = Rough\nTilesInSet = 10\nMorphable = false\nToTemperateTheater = 35\nLowRadarColor=120,120,150\nHighRadarColor=120,120,150\nRequiredForRMG = true\n\n[TileSet0041]\nSetName = Ramp edge fixup\nFileName = Rmpfx\nTilesInSet = 12\nMorphable = true\nToTemperateTheater = 43\nMarbleMadness = 42\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nRequiredForRMG = true\nAllowTiberium = true\n\n[TileSet0042]\nSetName = Ramp edge fixup - Marble Madness\nFileName = Mrmfx\nTilesInSet = 12\nMorphable = true\nToTemperateTheater = 44\nNonMarbleMadness = 41\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowTiberium = true\n\n[TileSet0043]\nSetName = Water slopes\nFileName = WSLOPE\nTilesInSet = 4\nMorphable = no\nToSnowTheater = 45\nMarbleMadness=59\nAllowBurrowing=false\n\n[TileSet0044]\nSetName = Paved Road Slopes\nFileName = Prslpe\nTilesInSet = 4\nMorphable = no\nToTemperateTheater = 47\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\n\n[TileSet0045]\nSetName = Monorail Slopes\nFileName = Tslope\nTilesInSet = 4\nMorphable = no\nToTemperateTheater = 48\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\nMarbleMadness=73\n\n[TileSet0046]\nSetName = Newest MM Height Pieces\nFileName = hyte\nTilesInSet = 15\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowTiberium = true\n\n[TileSet0047]\nSetName = Tunnel Floor\nFileName = tunnel\nTilesInSet = 4\nMorphable = no\nToTemperateTheater = 53\nLowRadarColor = 100,100,100\nHighRadarColor = 100,100,100\nAllowBurrowing=false\nMarbleMadness=71\n\n[TileSet0048]\nSetName = Tunnel Side\nFileName = tunnex\nTilesInSet = 2\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToTemperateTheater = 54\nAllowBurrowing=false\nMarbleMadness=72\n\n[TileSet0049]\nSetName = TrackTunnel Floor\nFileName = tunnet\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToTemperateTheater = 55\nAllowBurrowing=false\nMarbleMadness=71\n\n;\n; Dirt roads. Corner pieces. Marble Madness version.\n;\n[TileSet0050]\nSetName = MM Bendy Dirt Roads\nFileName = MDrodc\nTilesInSet = 24\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 16\nToTemperateTheater = 58\nAllowTiberium = true\n\n;\n; Dirt roads. Junctions.  Marble Madness version.\n;\n[TileSet0051]\nSetName = MM Dirt Road Junctions\nFileName = MDrodj\nTilesInSet = 11\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 17\nToTemperateTheater = 59\nAllowTiberium = true\n\n;\n; Dirt roads. Straights.  Marble Madness version.\n;\n[TileSet0052]\nSetName = MM Straight Dirt Roads\nFileName = MDrods\nTilesInSet = 66\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 18\nToTemperateTheater = 60\nAllowTiberium = true\n\n\n;\n; Shore pieces\n;\n[TileSet0053]\nSetName = Shore Pieces\nFileName = MShore\nTilesInSet = 42\nLowRadarColor=80,80,150\nHighRadarColor=80,80,150\nNonMarbleMadness=12\nAllowBurrowing=false\n\n\n;\n; Waterfalls. MM.\n;\n[TileSet0054]\nSetName = MM Waterfalls\nFileName = MWa-\nTilesInSet = 4\nToTemperateTheater = 62\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nNonMarbleMadness=30\nAllowBurrowing=false\n\n[TileSet0055]\nSetName = MM Waterfalls-B\nFileName = MWb-\nTilesInSet = 4\nToTemperateTheater = 63\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nNonMarbleMadness=35\nAllowBurrowing=false\n\n[TileSet0056]\nSetName = MM Waterfalls-C\nFileName = MWc-\nTilesInSet = 4\nToTemperateTheater = 64\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nNonMarbleMadness=36\nAllowBurrowing=false\n\n[TileSet0057]\nSetName = MM Waterfalls-D\nFileName = MWd-\nTilesInSet = 4\nToTemperateTheater = 65\nLowRadarColor=240,240,255\nHighRadarColor=240,240,255\nNonMarbleMadness=37\nAllowBurrowing=false\n\n;\n; Cliff pieces that meet water pieces\n;\n[TileSet0058]\nSetName = MM Cliff/Water pieces\nFileName = MWClif\nTilesInSet = 28\n;ShadowCaster = true\n;ShadowTiles = 22\nLowRadarColor=70,70,120\nHighRadarColor=90,90,200\nNonMarbleMadness=15\nToTemperateTheater=67\nAllowBurrowing=false\n\n\n[TileSet0059]\nSetName = MM Water slopes\nFileName = MWSLOP\nTilesInSet = 4\nMorphable = no\nToTemperateTheater = 68\nMarbleMadness=59\nNonMarbleMadness=43\nAllowBurrowing=false\n\n\n;\n; Just icy water.\n;\n[TileSet0060]\nSetName = MM Water\nFileName = MWater\nTilesInSet = 14\nLowRadarColor=10,10,80\nHighRadarColor=15,15,110\nNonMarbleMadness=21\nToTemperateTheater=69\nAllowBurrowing=false\n\n[TileSet0061]\nSetName = Destroyable Cliffs\nFileName = dcliff\nTilesInSet = 2\nMorphable = false\nLowRadarColor=120,120,150\nHighRadarColor=120,120,150\nToTemperateTheater=56\nAllowBurrowing=false\nMarbleMadness=74\n\n\n[TileSet0062]\nSetName = Rock LAT\nFileName = Rock\nTilesInSet = 1\nMorphable = false\nAllowBurrowing = false\nLowRadarColor = 10,90,90\nHighRadarColor = 10,128,128\nToTemperateTheater=33\nRequiredForRMG = true\n\n;\n; L.A.T. system for connecting rocky and normal terrain\n;\n[TileSet0063]\nSetName = Rock/Clear LAT\nFileName = rlat\nTilesInSet = 16\nMorphable = false\nAllowBurrowing = false\nLowRadarColor = 50,90,90\nHighRadarColor = 70,128,128\nAllowToPlace=no\nToTemperateTheater=34\nRequiredForRMG = true\n\n\n[TileSet0064]\nSetName = Grey\nFileName = Grey\nTilesInSet = 1\nMorphable = false\nAllowBurrowing = false\nLowRadarColor = 10,100,10\nHighRadarColor = 10,120,10\nToTemperateTheater=41\nRequiredForRMG = true\n\n;\n; L.A.T. system for connecting grey and normal terrain\n;\n[TileSet0065]\nSetName = Grey/Clear LAT\nFileName = glat\nTilesInSet = 16\nMorphable = false\nAllowBurrowing = false\nLowRadarColor = 40,90,0\nHighRadarColor = 80,110,0\nAllowToPlace=no\nToTemperateTheater=42\nRequiredForRMG = true\n\n[TileSet0066]\nSetName = DirtTrackTunnel Floor\nFileName = dtunnt\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToTemperateTheater=72\nAllowBurrowing=false\nMarbleMadness=71\n\n[TileSet0067]\nSetName = DirtTunnel Floor\nFileName = dtunn\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToTemperateTheater=73\nAllowBurrowing=false\nMarbleMadness=71\n\n[TileSet0068]\nSetName = Pavement (Use for LAT)\nFileName = Pvclr\nTilesInSet=1\nMorphable=no\nLowRadarColor = 128,128,128\nHighRadarColor = 128,128,128\nAllowBurrowing=false\nRequiredForRMG = true\nToTemperateTheater=46\n\n[TileSet0069]\nSetName = Pavement\nFileName = Pave\nTilesInSet = 14\nMorphable = false\nLowRadarColor = 128,128,128\nHighRadarColor = 128,128,128\nAllowBurrowing=false\nRequiredForRMG = true\nToTemperateTheater=38\n\n;\n; L.A.T. system for connecting pavement and normal terrain\n;\n[TileSet0070]\nSetName = Pavement/Clear LAT\nFileName = plat\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 110,80,40\nHighRadarColor = 150,100,65\nAllowToPlace=no\nAllowBurrowing=false\nRequiredForRMG = true\nToTemperateTheater=39\n\n;\n;New MarbleMadness set. \n;\n; MM Tunnel set\n[TileSet0071]\nSetName = MM Tunnel set\nFileName = mtunnl\nTilesInSet = 4\nShadowCaster = false\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nNonMarbleMadness=71\nAllowBurrowing=false\n\n[TileSet0072]\nSetName = MM Tunnel Side\nFileName = mtunnx\nTilesInSet = 2\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nNonMarbleMadness=48\n\n[TileSet0073]\nSetName = MM Monorail Slopes\nFileName = mtslop\nTilesInSet = 4\nMorphable = no\nNonMarbleMadness = 45\nLowRadarColor=92,92,92\nHighRadarColor=92,92,92\nAllowBurrowing=false\n\n[TileSet0074]\nSetName = MM Destroyable Cliffs\nFileName = mdclif\nTilesInSet = 2\nMorphable = false\nLowRadarColor=120,120,150\nHighRadarColor=120,120,150\nNonMarbleMadness=61\nAllowBurrowing=false\n\n[TileSet0075]\n; MM ice shore\n;Reserved  for future use \n;\n[TileSet0076]\n; MM ice lat 1\n;Reserved  for future use \n;\n[TileSet0077]\n; MM ice lat 2\n;Reserved  for future use \n;\n[TileSet0078]\n; MM ice lat 3\n;Reserved  for future use \n;\n\n\n;\n; Animating tiles\n;\n[Waterfalls]\nTile01Anim=WA01X\nTile01XOffset=5\nTile01YOffset=54\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WA02X\nTile02XOffset=-34\nTile02YOffset=41\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WA03X\nTile03XOffset=-27\nTile03YOffset=48\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WA04X\nTile04XOffset=-44\nTile04YOffset=39\nTile04AttachesTo=0\nTile04ZAdjust=0\n\n[Waterfalls-B]\nTile01Anim=WB01X\nTile01XOffset=39\nTile01YOffset=38\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WB02X\nTile02XOffset=36\nTile02YOffset=43\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WB03X\nTile03XOffset=26\nTile03YOffset=51\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WB04X\nTile04XOffset=14\nTile04YOffset=54\nTile04AttachesTo=0\nTile04ZAdjust=0\n\n[Waterfalls-C]\nTile01Anim=WC01X\nTile01XOffset=8\nTile01YOffset=16\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WC02X\nTile02XOffset=4\nTile02YOffset=-5\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WC03X\nTile03XOffset=13\nTile03YOffset=1\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WC04X\nTile04XOffset=-40\nTile04YOffset=-7\nTile04AttachesTo=1\nTile04ZAdjust=0\n\n[Waterfalls-D]\nTile01Anim=WD01X\nTile01XOffset=-7\nTile01YOffset=-8\nTile01AttachesTo=1\nTile01ZAdjust=0\nTile02Anim=WD02X\nTile02XOffset=-1\nTile02YOffset=-8\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WD03X\nTile03XOffset=-13\nTile03YOffset=1\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WD04X\nTile04XOffset=0\nTile04YOffset=17\nTile04AttachesTo=0\nTile04ZAdjust=0\n\n[Tunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[TrackTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[DirtTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[DirtTrackTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n"
  },
  {
    "path": "DXMainClient/Resources/INI/sound.ini",
    "content": "****SOUND.INI IS OBSOLETE****\nThe game no longer reads this file. Use SOUND01.INI instead.\nIf you try to add any new sounds here, they won't be played in-game."
  },
  {
    "path": "DXMainClient/Resources/INI/sound01.ini",
    "content": "; SOUND.INI\n; All sounds in the game are specified here. The sound must\n; first be specified in the sound list section. Then the appropriate\n; sound data should be specified for those sounds that differ from\n; the default ratings.\n\n; All sounds in the game are listed here (by name).\n[SoundList]\n000=FIRSTRM1\t\t;Firestorm defense burning\n001=FACBLD1     ;<FACTORY GOES ONLINE>\n002=ION1\t\t\t\t;Ion cannon strike\n003=PLSECAN2    ;<PULSE CANNON FIRES>\n004=METEOR1     ;LARGE METEOR\n005=METEOR2     ;SMALL METEOR\n006=METHIT1\t\t;METEOR HITS GROUND\n007=ICBM1\t\t\t;BIG, HUGE, ICBM ROCKET\n008=DEDMAN1\n009=DEDMAN2\n010=DEDMAN3\n011=DEDMAN4\n012=NOTIFY\n013=GUN18\t\t;Civilian gun\n014=SSPLASH1\t\t;<SMALL WATER SPLASH>\n015=SSPLASH2\t\t;<SMALL WATER SPLASH>\n016=SSPLASH3\t\t;<SMALL WATER SPLASH>\n017=dsaping1\t;Deployable sensor array ping\n018=DEDMAN5\n019=ORCAUP1\t\t;ORCA TAKES OFF\n020=ORCADWN1\t\t;ORCA LANDS\n021=DROPUP1\t\t;DROPSHIP TAKES OFF\n022=DROPDWN1\t\t;DROPSHIP LANDS\n023=CRMBLE2\t\t;Building crumbling\n024=HOVRMIS1\t\t\t;HOVERMLRS ROCKET FIRE\n025=GLNCH4\t\t\t;RPG launch\n026=REPAIR11\t\t;REPAIR VEHICLE\n027=OBELPOWR\t\t;obelisk\n028=SQUISHY2\t\t;placeholder for real guy squish\n029=SCOLD8\t\t\t;Scold sound\n030=COMMUP1\t\t;COMMUNICATIONS CENTER GOES ONLINE\n031=RADARDN1\t\t;placeholder for communication failure\n032=PLACE2\t\t\t;PLACE A BUILDING DOWN\n033=EXPNEW01\t\t;<BIG BUILDING EXPLOSION WITH GLASS>\n034=EXPNEW02\t\t;<BIG BUILDING EXPLOSION WITH GLASS>\n035=EXPNEW03\t\t;<BIG BUILDING EXPLOSION WITH GLASS>\n036=EXPNEW04\t;<BIG BUILDING EXPLOSION WITH GLASS>\n037=EXPNEW05\t\t;<MEDIUM EXPLOSION FOR VEHICLE OR STRUCTURE>\n038=EXPNEW06\t\t;<MEDIUM EXPLOSION GOOD FOR ALL AND LAND DESTRUCTION>\n039=EXPNEW07\t\t;<MEDIUM EXPLOSION WITH GLASS>\n040=EXPNEW08\t\t;<LG/MD EXPLOSION WITH GLASS>\n041=EXPNEW09\t\t;<MEDIUM EXPLOSION GENERAL>\n042=EXPNEW10\t\t;<MEDIUM EXPLOSION GENERAL AND VEHICLE>\n043=EXPNEW11\t\t;<MEDIUM EXPLOSION WITH GLASS>\n044=EXPNEW12\t\t;<SMALL EXPLOSION GENERAL>\n045=EXPNEW13\t\t;<SMALL EXPLOSION GENERAL>\n046=EXPNEW14\t\t;<SMALL EXPLOSION GENERAL>\n047=EXPNEW15\t\t;<SMALL EXPLOSION GENERAL>\n048=CASHTURN\t\t;placeholder for cashturn\n049=CREDUP1\t\t;CREDIT POSITIVE\n050=CREDDWN1\t\t;CREDIT NEGATIVE\n051=GATEDWN1\t\t;Gate going down\n052=GATEUP1\t\t;Gate going up\n053=SUBDRIL1\t\t;Subterrian drill\n054=SONIC4\t\t\t;Sonic weapon fire\n055=OBELRAY1\t\t;Obelisk firing laser\n056=120MMF\t\t\t;Artillery sound\n057=INFGUN3\t\t;Infantry gun\n058=VICER1\t\t\t;<TWO VICEROIDS COMBINE>\n059=CHAINGN1\t;BUGGY FIRES GUN/APACHE FIRES GUNS\n060=CYGUN1\t\t;CYBORG FIRES GUN\n061=RKETINF1\t;ROCKET INFANTRY FIRE\n062=JUMPJET1\t;JUMP JET FIRES GUN\n063=ORCAMIS1\t;ORCA FIGHTER ATTACK\n064=SAMSHOT1\t;SAM SITE MISSILE\n065=TSGUN4\n066=SILENCER\t;Sniper gun\n067=120mmx9\t;Artillery sound two-shooter\n068=BLEEP1\t\t;Generic beep\n069=CLICKY1\t;Generic click\n070=CLOAK5\t\t;Cloaking sound\n071=GAMEFRM1\t;Game forming\n072=GOSTGUN1\t;Ghost talker\n073=HEALER1\t;Healing units\n074=MESSAGE1\t;Incomming message\n075=ICECRAK1\n076=ICECRAK2\n077=ICECRAK3\n078=MISL1\n079=SCRIN5B\n080=BIGGGUN1\n081=SLVKGUN1\n082=SQUISH6\n083=SANDBAG1\n084=FIEND1\n085=FIEND2\n086=EXPDIRT1\n087=ELECTRO1\n088=EXPNEW16\n089=EXPNEW17\n090=EXPNEW18\n091=EXPNEW19\n092=15-I000\t;Infantry reporting\n093=15-I002\t;Unit ready!\n094=15-I004\t;Awaiting order\n095=15-I006\t;Sir?\n096=15-I008\t;Sir, yes sir!\n097=15-I010\t;Ready\n098=15-I012\t;Yes sir\n099=15-I014\t;Yes sir!\n100=15-I016\t;Orders received\n101=15-I018\t;Moving out\n102=15-I020\t;Advancing\n103=15-I022\t;On my way\n104=15-I024\t;You got it\n105=15-I026\t;No problem\n106=15-I032\t;Ready for action\n107=FLAMTNK1 ; Flame tank fire\n108=RAILUSE5\t;Heavy mech railgun\n109=15-I038\t;Standing by\n110=15-I040\t;Yeah\n111=15-I042\t;Orders?\n112=15-I044\t;Load and clear\n113=15-I046\t;I'm on it\n114=15-I048\t;Sir!\n115=15-I050\t;Good as done\n116=15-I058\t;I'm taking heavy fire\n117=15-I060\t;Move! Move! Move!\n118=15-I064\t;MEDIC!\n119=11-I000\t;Oxanna - Yes\n120=11-I002\t;Direct me\n121=11-I004\t;Awaiting orders\n122=11-I006\t;I'm ready\n123=11-I008\t;Of course\n124=11-I010\t;Immediatly\n125=11-I012\t;Yes!\n126=11-I014\t;For Kane\n127=11-I016\t;They will pay for this\n128=11-I018\t;For the brotherhood\n129=12-I000\t;Slavik\n130=12-I002\n131=12-I004\n132=12-I006\n133=12-I008\n134=12-I010\n135=12-I012\n136=12-I014\n137=12-I016\n138=13-I000\t;Tratos\n139=13-I002\n140=13-I004\n141=13-I006\n142=13-I008\n143=13-I010\n144=13-I012\n145=13-I014\n146=13-I016\n147=13-I018\n148=13-I020\n149=14-I000\t;Ghost stalker\n150=14-I002\n151=14-I004\n152=WRONG1\t;Build queue full\n153=14-I008\n154=14-I010\n155=14-I012\n156=14-I014\n157=14-I016\n158=21-I000\t;Spy\n159=21-I002\n160=21-I004\n161=KLAX1\t;Klaxon\n162=27-I002\t;Unit deploy response\n163=21-I010\n164=21-I012\n165=HUNTER2\n166=21-I016\n167=LASTUR1\n168=21-I022\n169=22-I000\t;Cyborg\n170=22-I002\n171=22-I006\n172=22-I008\n173=22-I010\n174=22-I012\n175=22-I014\n176=22-I016\n177=22-I018\n178=22-I020\n179=23-I000\t;Cyborg commando\n180=23-I002\n181=23-I004\n182=23-I006\n183=23-I008\n184=23-I010\n185=23-I012\n186=23-I014\n187=23-I016\n188=23-I018\n189=23-I020\n190=23-I022\n191=24-I000\t;Mutant hijacker\n192=24-I002\n193=24-I004\n194=24-I006\n195=24-I008\n196=24-I010\n197=24-I012\n198=24-I014\n199=24-I016\n200=24-I018\n201=24-I020\n202=24-I022\n203=24-I024\n204=32-I000\t;Banshee\n205=32-I002\n206=32-I004\n207=32-I006\n208=32-I008\n209=09-I000\n210=09-I002\n211=09-I004\n212=09-I006\n213=19-I000 ; Engineer\n214=19-I002\n215=19-I006\n216=19-I010\n217=19-I016\n218=19-I018\n219=10-I000\t;Umagon\n220=10-I002\n221=10-I004\n222=10-I006\n223=10-I016\n224=10-I020\n225=10-I022\n226=10-I024\n227=10-I026\n228=10-I028\n229=10-I030\n230=DEDMAN6\n231=DEDGIRL1\n232=DEDGIRL2\n233=DEDGIRL3\n234=DEDGIRL4\n235=20-I000\n236=20-I004\n237=20-I006\n238=20-I008\n239=20-I010\n240=20-I012\n241=20-I016\n242=20-I018\n243=20-I020\n244=25-I000\n245=25-I002\n246=25-I004\n247=25-I006\n248=25-I012\n249=25-I014\n250=25-I016\n251=25-I018\n252=25-I022\n253=25-I024\n254=25-I026\n255=30-I000\n256=30-I002\n257=30-I004\n258=30-I006\n259=30-I014\n260=30-I016\n261=30-I018\n262=30-I022\n263=30-I030\n264=30-I034\n265=30-I036\n266=42-I000\t;Toxin soldier\n267=42-I002\n268=42-I004\n269=42-I006\n270=42-I008\n271=42-I010\n272=42-I012\n273=60-N100 ; Firestorm additions start here\n274=60-N102\n275=60-N104\n276=60-N106\n277=60-N108\n278=60-N110\n279=60-N112\n280=60-N114\n281=60-N116\n282=53-I000\n283=53-I002\n284=53-I004\n285=53-I006\n286=53-I008\n287=53-I010\n288=53-I012\n289=54-N022\n290=54-N024\n291=54-N026\n292=54-N028\n293=54-N030\n294=67-N100\n295=67-N102\n296=67-N104\n297=67-N106\n298=67-N108\n299=68-N100\n300=68-N102\n301=68-N104\n302=68-N106\n303=68-N108\n304=68-N110\n305=69-N100\n306=69-N102\n307=69-N104\n308=69-N106\n309=69-N108\n310=69-N110\n311=70-N000\n312=70-N002\n313=70-N004\n314=70-N006\n315=70-N008\n316=70-N010\n317=70-N012\n318=70-N014\n319=70-N016\n320=70-N018\n321=COREFIR1\n322=COREUP1\n323=FIREWEB1\n324=FLOATMOV\n325=JUGGER1\n326=LIMPBOM1\n327=LIMPC3\n328=LIMPQ3\n329=SPIDDIE1\n330=MOBEMP1\n331=MSG1\n332=OBELMOD1\n333=22-N104\n334=22-N106\n335=22-N108\n336=LIMPC4\n337=LIMPQ4\n338=FLOTMOV2\n339=FLOTMOV3\n340=FLOTMOV4\n341=FLOATK1\n342=OBELCOR3\n999=BOOP\t\t;Stub sound used for all unsigned sound trigers\n\n\n; ******* Individual Sound Overrides *******\n; These sections are used to specify overrides from\n; the default values for sound effects.\n;\n; Priority = Priority adjustment from normal (def=10)\n;            Increasing numbers have higher priority.\n; Volume = volume to play sound at (def=1.0)\n\n[FIRSTRM1]\n\n[FACBLD1]\n\n[ION1]\nPriority=75\n\n[PLSECAN2]\n\n[METEOR1]\nPriority=75\n\n[METEOR2]\nPriority=75\n\n[METHIT1]\nPriority=75\n\n[ICBM1]\nPriority=75\n\n[DEDMAN1]\nPriority=50\n\n[DEDMAN2]\nPriority=50\n\n[DEDMAN3]\nPriority=50\n\n[DEDMAN4]\nPriority=50\n\n[NOTIFY]\n\n[GUN18]\n\n[SSPLASH1]\n\n[SSPLASH2]\n\n[SSPLASH3]\n\n[DSAPING1]\n\n[DEDMAN5]\nPriority=50\n\n[ORCAUP1]\n\n[ORCADWN1]\n\n[DROPUP1]\n\n[DROPDWN1]\n\n[CRMBLE2]\n\n[HOVRMIS1]\n\n[GLNCH4]\n\n[REPAIR11]\n\n[OBELPOWR]\n\n[SQUISHY2]\nPriority=50\n\n[SCOLD8]\n\n[COMMUP1]\n\n[RADARDN1]\n\n[PLACE2]\n\n[EXPNEW01]\nPriority=50\n\n[EXPNEW02]\nPriority=50\n\n[EXPNEW03]\nPriority=50\n\n[EXPNEW04]\nPriority=50\n\n[EXPNEW05]\nPriority=50\n\n[EXPNEW06]\nPriority=50\n\n[EXPNEW07]\nPriority=50\n\n[EXPNEW08]\nPriority=50\n\n[EXPNEW09]\nPriority=50\n\n[EXPNEW10]\nPriority=50\n\n[EXPNEW11]\nPriority=50\n\n[EXPNEW12]\nPriority=50\n\n[EXPNEW13]\nPriority=50\n\n[EXPNEW14]\nPriority=50\n\n[EXPNEW15]\nPriority=50\n\n[CASHTURN]\n\n[CREDUP1]\nPriority=15\n\n[CREDDWN1]\n\n[GATEDWN1]\n\n[GATEUP1]\n\n[SUBDRIL1]\n\n[SONIC4]\n\n[OBELRAY1]\n\n[120MMF]\n\n[INFGUN3]\n\n[VICER1]\n\n[CHAINGN1]\n\n[CYGUN1]\n\n[RKETINF1]\n\n[JUMPJET1]\n\n[ORCAMIS1]\n\n[SAMSHOT1]\n\n[TSGUN4]\n\n[SILENCER]\n\n[120mmx9]\n\n[BLEEP1]\n\n[CLICKY1]\n\n[CLOAK5]\n\n[GAMEFRM1]\n\n[GOSTGUN1]\n\n[HEALER1]\n\n[MESSAGE1]\nPriority=15\n\n[ICECRAK1]\n\n[ICECRAK2]\n\n[ICECRAK3]\n\n[MISL1]\n\n[SCRIN5B]\n\n[BIGGGUN1]\n\n[SLVKGUN1]\n\n[SQUISH6]\nPriority=55\n\n[SANDBAG1]\n\n[FIEND1]\n\n[FIEND2]\n\n[LASTUR1]\nPriority=25\n\n[EXPDIRT1]\nPriority=50\n\n[ELECTRO1]\nPriority=50\n\n[EXPNEW16]\nPriority=50\n\n[EXPNEW17]\nPriority=50\n\n[EXPNEW18]\nPriority=50\n\n[EXPNEW19]\nPriority=50\n\n[15-I000]\nPriority=100\n\n[15-I002]\nPriority=100\n\n[15-I004]\nPriority=100\n\n[15-I006]\nPriority=100\n\n[15-I008]\nPriority=100\n\n[15-I010]\nPriority=100\n\n[15-I012]\nPriority=100\n\n[15-I014]\nPriority=100\n\n[15-I016]\nPriority=100\n\n[15-I018]\nPriority=100\n\n[15-I020]\nPriority=100\n\n[15-I022]\nPriority=100\n\n[15-I024]\nPriority=100\n\n[15-I026]\nPriority=100\n\n[15-I032]\nPriority=100\n\n[FLAMTNK1]\n\n[RAILUSE5]\n\n[15-I038]\nPriority=100\n\n[15-I040]\nPriority=100\n\n[15-I042]\nPriority=100\n\n[15-I044]\nPriority=100\n\n[15-I046]\nPriority=100\n\n[15-I048]\nPriority=100\n\n[15-I050]\nPriority=100\n\n[15-I058]\nPriority=100\n\n[15-I060]\nPriority=100\n\n[15-I064]\nPriority=100\n\n[11-I000]\nPriority=100\n\n[11-I002]\nPriority=100\n\n[11-I004]\nPriority=100\n\n[11-I006]\nPriority=100\n\n[11-I008]\nPriority=100\n\n[11-I010]\nPriority=100\n\n[11-I012]\nPriority=100\n\n[11-I014]\nPriority=100\n\n[11-I016]\nPriority=100\n\n[11-I018]\nPriority=100\n\n[12-I000]\nPriority=100\n\n[12-I002]\nPriority=100\n\n[12-I004]\nPriority=100\n\n[12-I006]\nPriority=100\n\n[12-I008]\nPriority=100\n\n[12-I010]\nPriority=100\n\n[12-I012]\nPriority=100\n\n[12-I014]\nPriority=100\n\n[12-I016]\nPriority=100\n\n[13-I000]\nPriority=100\n\n[13-I002]\nPriority=100\n\n[13-I004]\nPriority=100\n\n[13-I006]\nPriority=100\n\n[13-I008]\nPriority=100\n\n[13-I010]\nPriority=100\n\n[13-I012]\nPriority=100\n\n[13-I014]\nPriority=100\n\n[13-I016]\nPriority=100\n\n[13-I018]\nPriority=100\n\n[13-I020]\nPriority=100\n\n[14-I000]\nPriority=100\n\n[14-I002]\nPriority=100\n\n[14-I004]\nPriority=100\n\n[WRONG1]\n\n[14-I008]\nPriority=100\n\n[14-I010]\nPriority=100\n\n[14-I012]\nPriority=100\n\n[14-I014]\nPriority=100\n\n[14-I016]\nPriority=100\n\n[21-I000]\nPriority=100\n\n[21-I002]\nPriority=100\n\n[21-I004]\nPriority=100\n\n[KLAX1]\n\n[27-I002]\nPriority=100\n\n[21-I010]\nPriority=100\n\n[21-I012]\nPriority=100\n\n[HUNTER2]\nPriority=75\n\n[21-I016]\nPriority=100\n\n[21-I022]\nPriority=100\n\n[22-I000]\nPriority=100\n\n[22-I002]\nPriority=100\n\n[22-I006]\nPriority=100\n\n[22-I008]\nPriority=100\n\n[22-I010]\nPriority=100\n\n[22-I012]\nPriority=100\n\n[22-I014]\nPriority=100\n\n[22-I016]\nPriority=100\n\n[22-I018]\nPriority=100\n\n[22-I020]\nPriority=100\n\n[23-I000]\nPriority=100\n\n[23-I002]\nPriority=100\n\n[23-I004]\nPriority=100\n\n[23-I006]\nPriority=100\n\n[23-I008]\nPriority=100\n\n[23-I010]\nPriority=100\n\n[23-I012]\nPriority=100\n\n[23-I014]\nPriority=100\n\n[23-I016]\nPriority=100\n\n[23-I018]\nPriority=100\n\n[23-I020]\nPriority=100\n\n[23-I022]\nPriority=100\n\n[24-I000]\nPriority=100\n\n[24-I002]\nPriority=100\n\n[24-I004]\nPriority=100\n\n[24-I006]\nPriority=100\n\n[24-I008]\nPriority=100\n\n[24-I010]\nPriority=100\n\n[24-I012]\nPriority=100\n\n[24-I014]\nPriority=100\n\n[24-I016]\nPriority=100\n\n[24-I018]\nPriority=100\n\n[24-I020]\nPriority=100\n\n[24-I022]\nPriority=100\n\n[24-I024]\nPriority=100\n\n[32-I000]\nPriority=100\n\n[32-I002]\nPriority=100\n\n[32-I004]\nPriority=100\n\n[32-I006]\nPriority=100\n\n[32-I008]\nPriority=100\n\n[09-I000]\nPriority=100\n\n[09-I002]\nPriority=100\n\n[09-I004]\nPriority=100\n\n[09-I006]\nPriority=100\n\n[19-I000]\nPriority=100\n\n[19-I002]\nPriority=100\n\n[19-I006]\nPriority=100\n\n[19-I010]\nPriority=100\n\n[19-I016]\nPriority=100\n\n[19-I018]\nPriority=100\n\n[10-I000]\nPriority=100\n\n[10-I002]\nPriority=100\n\n[10-I004]\nPriority=100\n\n[10-I006]\nPriority=100\n\n[10-I016]\nPriority=100\n\n[10-I020]\nPriority=100\n\n[10-I022]\nPriority=100\n\n[10-I024]\nPriority=100\n\n[10-I026]\nPriority=100\n\n[10-I028]\nPriority=100\n\n[10-I030]\nPriority=100\n\n[DEDMAN6]\nPriority=50\n\n[DEDGIRL1]\nPriority=50\n\n[DEDGIRL2]\nPriority=50\n\n[DEDGIRL3]\nPriority=50\n\n[DEDGIRL4]\nPriority=50\n\n[20-I000]\nPriority=100\n\n[20-I004]\nPriority=100\n\n[20-I006]\nPriority=100\n\n[20-I008]\nPriority=100\n\n[20-I010]\nPriority=100\n\n[20-I012]\nPriority=100\n\n[20-I016]\nPriority=100\n\n[20-I018]\nPriority=100\n\n[20-I020]\nPriority=100\n\n[25-I000]\nPriority=100\n\n[25-I002]\nPriority=100\n\n[25-I004]\nPriority=100\n\n[25-I006]\nPriority=100\n\n[25-I012]\nPriority=100\n\n[25-I014]\nPriority=100\n\n[25-I016]\nPriority=100\n\n[25-I018]\nPriority=100\n\n[25-I022]\nPriority=100\n\n[25-I024]\nPriority=100\n\n[25-I026]\nPriority=100\n\n[30-I000]\nPriority=100\n\n[30-I002]\nPriority=100\n\n[30-I004]\nPriority=100\n\n[30-I006]\nPriority=100\n\n[30-I014]\nPriority=100\n\n[30-I016]\nPriority=100\n\n[30-I018]\nPriority=100\n\n[30-I022]\nPriority=100\n\n[30-I030]\nPriority=100\n\n[30-I034]\nPriority=100\n\n[30-I036]\nPriority=100\n\n[42-I000]\nPriority=100\n\n[42-I002]\nPriority=100\n\n[42-I004]\nPriority=100\n\n[42-I006]\nPriority=100\n\n[42-I008]\nPriority=100\n\n[42-I010]\nPriority=100\n\n[42-I012]\nPriority=100\n\n\n; Firestorm additions start here\n[60-N100]\nPriority=100\n\n[60-N102]\nPriority=100\n\n[60-N104]\nPriority=100\n\n[60-N106]\nPriority=100\n\n[60-N108]\nPriority=100\n\n[60-N110]\nPriority=100\n\n[60-N112]\nPriority=100\n\n[60-N114]\nPriority=100\n\n[60-N116]\nPriority=100\n\n[53-I000]\nPriority=100\n\n[53-I002]\nPriority=100\n\n[53-I004]\nPriority=100\n\n[53-I006]\nPriority=100\n\n[53-I008]\nPriority=100\n\n[53-I010]\nPriority=100\n\n[53-I012]\nPriority=100\n\n[54-N022]\nPriority=100\n\n[54-N024]\nPriority=100\n\n[54-N026]\nPriority=100\n\n[54-N028]\nPriority=100\n\n[54-N030]\nPriority=100\n\n[67-N100]\nPriority=100\n\n[67-N102]\nPriority=100\n\n[67-N104]\nPriority=100\n\n[67-N106]\nPriority=100\n\n[67-N108]\nPriority=100\n\n[68-N100]\nPriority=100\n\n[68-N102]\nPriority=100\n\n[68-N104]\nPriority=100\n\n[68-N106]\nPriority=100\n\n[68-N108]\nPriority=100\n\n[68-N110]\nPriority=100\n\n[69-N100]\nPriority=100\n\n[69-N102]\nPriority=100\n\n[69-N104]\nPriority=100\n\n[69-N106]\nPriority=100\n\n[69-N108]\nPriority=100\n\n[69-N110]\nPriority=100\n\n[70-N000]\nPriority=100\n\n[70-N002]\nPriority=100\n\n[70-N004]\nPriority=100\n\n[70-N006]\nPriority=100\n\n[70-N008]\nPriority=100\n\n[70-N010]\nPriority=100\n\n[70-N012]\nPriority=100\n\n[70-N014]\nPriority=100\n\n[70-N016]\nPriority=100\n\n[70-N018]\nPriority=100\n\n[COREFIR1]\nPriority=75\n\n[COREUP1]\nPriority=100\n\n[FIREWEB1]\nPriority=75\n\n[FLOATMOV]\nPriority=50\n\n[JUGGER1]\nPriority=75\n\n[LIMPBOM1]\nPriority=100\n\n[LIMPC3]\nPriority=100\n\n[LIMPQ3]\nPriority=100\n\n[SPIDDIE1]\nPriority=75\n\n[MOBEMP1]\nPriority=75\n\n[MSG1]\nPriority=100\n\n[OBELMOD1]\nPriority=75\n\n[22-N104]\nPriority=100\n\n[22-N106]\nPriority=100\n\n[22-N108]\nPriority=100\n\n[LIMPC4]\nPriority=100\n\n[LIMPQ4]\nPriority=100\n\n[FLOTMOV2]\nPriority=30\n\n[FLOTMOV3]\nPriority=30\n\n[FLOTMOV4]\nPriority=30\n\n[FLOATK1]\nPriority=30\n\n[OBELCOR3]\n"
  },
  {
    "path": "DXMainClient/Resources/INI/temperat.ini",
    "content": ";Modified July 30, 2002\n;Marble madness set added\n;By DJBREIT\n: Version 1.0\n;\n;           ***Tiberian Sun Isometric Tile Control File***\n;\n\n\n;\n; General section.\n;\n; RampBase\n;  Number of tile set that includes all the ramp types\n;\n; MMRampBase\n;  Number of tile set that has the marble madness mode ramps\n;\n; ClearTile\n;  Number of tile set to use for clear terrain\n;\n; RoughTile\n;  Number of tile set that has the rough terrain\n;\n; ClearToRoughLAT\n;  Tile set that has the 16 tiles for the clear/rough LAT system\n;\n; HeightBase\n;  First tile of marble madness height tiles\n;\n; BlackTile\n;  Black tile used when rendering non-existent cells\n;\n; BridgeSet\n;  Tile set that contains bridge edges\n;\n; BridgeTopLeft1\n; BridgeTopLeft2\n; BridgeBottomRight1\n; BridgeBottomRight2\n; BridgeTopRight1\n; BridgeTopRight2\n; BridgeBottomLeft1\n; BridgeBottomLeft2\n;  Tiles in bridge set to search for when fixing up bridges\n;\n;\n\n[General]\nRampBase = 9\nRampSmooth = 43\nMMRampBase = 7\nClearTile = 0\nRoughTile = 13\nClearToRoughLat = 14\nSandTile = 33\nClearToSandLat = 34\nGreenTile = 41\nClearToGreenLat = 42\nPaveTile = 46\nMiscPaveTile = 38\nClearToPaveLat = 39\nHeightBase = 52\nBlackTile = 6\nCliffSet = 10\nShorePieces = 12\nWaterSet = 21\nIce1Set = 31\nIce2Set = 32\nIceShoreSet = 32\nBridgeSet = 19\nTrainBridgeSet = 37\nSlopeSetPieces = 25\nSlopeSetPieces2 = 26\nMonorailSlopes = 48\nTunnels = 53\nTrackTunnels = 55\nDirtTunnels = 72\nDirtTrackTunnels = 73\nWaterfallEast = 49\nWaterfallWest = 51\nWaterfallNorth = 50\nWaterfallSouth = 30\nCliffRamps = 25\nPavedRoads = 20\nPavedRoadEnds = 36\nMedians = 40\nRoughGround=35\nDirtRoadJunction = 17\nDirtRoadCurve = 16\nDirtRoadStraight = 18\nBridgeTopLeft1 = 1\nBridgeTopLeft2 = 2\nBridgeBottomRight1 = 3\nBridgeBottomRight2 = 3\nBridgeTopRight1 = 4\nBridgeTopRight2 = 5\nBridgeBottomLeft1 = 6\nBridgeBottomLeft2 = 6\nBridgeMiddle1 = 7\nBridgeMiddle2 = 12\nDestroyableCliffs = 56\nWaterCliffs = 15\nWaterCaves = 57\nPavedRoadSlopes = 47\nDirtRoadSlopes = 23\nCrystalTile = 74\nClearToCrystalLat = 75\nSwampTile = 76\nWaterToSwampLat = 77\nBlueMoldTile = 78\nClearToBlueMoldLat = 79\nCrystalCliff = 80\n\n;\n; TS Will scan through this file when loading up a theater and read in the\n; isometric tile files specified.\n;\n; [TileSetnnnn]\n;  This is the tile set section header. TS will loop through from TileSet0000\n;  upwards until it finds a set that hasnt been specified.\n;\n; SetName\n;  The name of the set as it will appear in the editor.\n;\n; FileName\n;  The base file name of each file in the set. The files in a set must all\n;  have the same basic name with a 2 digit id number appended. For example\n;  cliff01.tem, cliff02.tem, cliff03.tem. The 2 digit number starts at 01\n;  not 00.\n;\n; TilesInSet\n;  The number of files comprising the set. There is a practical limit of\n;  99 due to the 2 digit file name suffix.\n;\n; LastTilesInSet\n;  The number of tiles which the set used to have. This tells the\n;  game that the number of tiles in the set has changed and it should fix up\n;  the tile numbers when a map is loaded. If the map is then saved again,\n;  it will be saved with the correct tile numbers. This value should only\n;  be used to load up maps, convert the tile numbers, then save the maps\n;  out again. Then the LastTilesInSet entry should be removed or the newly\n;  fixed up maps will not load correctly.\n;\n; MarbleMadness\n;  The section number of the tile set to use for these tiles when in\n;  marble madness mode.\n;\n; NonMarbleMadness\n;  For marble madness tiles, this is the tile set to use when not in\n;  marble madness mode.\n;\n; Morphable\n;  Can this tile set be modified using the raise/lower ground function?\n;\n; ShadowCaster\n;  Do the tiles in this set cast shadows (cliff pieces)\n;\n; ToTemperateTheater\n;  The equivilent tile section in the temperate theater\n;\n; ToSnowTheater\n;  The equivilent tile section in the snow theater\n;\n; LowRadarColor\n;\tWhat color to show on the radar for this set at the lowest height\n;\n; HighRadarColor\n;\tWhat color to show on the radar for this set at the highest height\n;\n; AllowToPlace\n;\tShould this tile be visible in the placement dialogue (def = true)?\n\n\n\n;\n; Blank tile for filling in holes.\n;\n[TileSet0000]\nSetName = Clear\nFileName = Clear\nTilesInSet = 1\nMorphable = true\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nRequiredForRMG = true\nAllowBurrowing = true\nAllowTiberium = true\n\n;\n; A few buildings\n;\n[TileSet0001]\nSetName = Misc Buildings\nFileName = Bld\nTilesInSet = 3\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nAllowToPlace=no\nAllowBurrowing=false\n\n;\n; Some basic flat tiles\n;\n[TileSet0002]\nSetName = Clear\nFileName = Snow\nTilesInSet = 4\nMorphable = true\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; A couple of old cliff pieces (not used)\n;\n[TileSet0003]\nSetName = Cliff Pieces\nFileName = clif\nTilesInSet = 2\nAllowToPlace=no\nAllowBurrowing=false\n\n;\n; A large ice flow.\n;\n[TileSet0004]\nSetName = Ice Flow\nFileName = flow\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; A nice little house.\n;\n[TileSet0005]\nSetName = House\nFileName = house\nTilesInSet = 1\nAllowToPlace=no\nAllowBurrowing=false\n\n;\n; Blank tile used for filling areas with no cell data.\n;\n[TileSet0006]\nSetName = Blank\nFileName = blank\nTilesInSet = 1\nMorphable = true\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nAllowToPlace=no\nAllowBurrowing=false\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Marble madness mode ramp pieces.\n;\n[TileSet0007]\nSetName = MM Ramps\nFileName = mslop\nTilesInSet = 20\nNonMarbleMadness = 9\nMorphable = true\n;LastTilesInSet = 16\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nAllowTiberium = true\n\n;\n; Height pieces for marble madness mode.\n;\n; Obsolete. Replaced with HITE01 - HITE10\n;\n[TileSet0008]\nSetName = Obsolete Height Pieces\nFileName = hyte\nTilesInSet = 7\nMorphable = true\nAllowToPlace=no\nAllowTiberium = true\n\n;\n; Misc theater ramps\n;\n[TileSet0009]\nSetName = Ramps\nFileName = slope\nTilesInSet = 20\nMarbleMadness = 7\nMorphable = true\n;LastTilesInSet = 16\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Cliff set.\n;\n[TileSet0010]\nSetName = Cliff Set\nFileName = Cliff\nTilesInSet = 40\nMarbleMadness = 22\nShadowCaster = true\nShadowTiles = 40\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Civilian buildings\n;\n[TileSet0011]\nSetName = Civilian Buildings\nFileName = Civ\nTilesInSet = 8\nAllowToPlace=no\nAllowBurrowing=false\n\n;\n; Shore pieces\n;\n[TileSet0012]\nSetName = Shore Pieces\nFileName = Shore\nTilesInSet = 42\nLowRadarColor = 30,30,40\nHighRadarColor = 30,30,40\nMarbleMadness = 61\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Clear terrain (slightly rough)\n;\n[TileSet0013]\nSetName = Rough LAT tile\nFileName = Ruff\nTilesInSet = 1\nMorphable = true\nLowRadarColor = 70,40,0\nHighRadarColor = 80,45,0\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; L.A.T. system for connecting clear and rough clear terrain\n;\n[TileSet0014]\nSetName = Clear/Rough LAT\nFileName = clat\nTilesInSet = 16\nMorphable = true\nLowRadarColor = 80,45,0\nHighRadarColor = 100,60,0\nAllowToPlace=no\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Cliff pieces that meet water pieces\n;\n[TileSet0015]\nSetName = Cliff/Water pieces\nFileName = WCliff\nTilesInSet = 28\nShadowCaster = true\nShadowTiles = 28\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nMarbleMadness=67\nAllowBurrowing=false\n\n;\n; Dirt roads. Corner pieces.\n;\n[TileSet0016]\nSetName = Bendy Dirt Roads\nFileName = Droadc\nTilesInSet = 24\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nMarbleMadness = 58\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Dirt roads. Junctions.\n;\n[TileSet0017]\nSetName = Dirt Road Junctions\nFileName = Droadj\nTilesInSet = 11\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nMarbleMadness = 59\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Dirt roads. Straights.\n;\n[TileSet0018]\nSetName = Straight Dirt Roads\nFileName = Droads\nTilesInSet = 66\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nMarbleMadness = 60\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Bridge sections.\n;\n[TileSet0019]\nSetName = Bridges\nFileName = Ovrps\nTilesInSet = 16\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\n\n;\n; Paved roads.\n;\n[TileSet0020]\nSetName = Paved Roads\nFileName = Proad\nTilesInSet = 21\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Just icy water.\n;\n[TileSet0021]\nSetName = Water\nFileName = Water\nTilesInSet = 14\nLowRadarColor = 10,10,30\nHighRadarColor = 10,10,50\nMarbleMadness=69\nAllowBurrowing=false\nRequiredForRMG = true\n\n;\n; Cliff set.\n;\n[TileSet0022]\nSetName = Marble Madness Cliff Set\nFileName = Mclif\nTilesInSet = 40\nNonMarbleMadness = 10\nShadowCaster = true\nShadowTiles = 40\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nAllowBurrowing=false\n\n;\n; Dirt road slopes\n;\n[TileSet0023]\nSetName = Dirt Road Slopes\nFileName = DRSLPE\nTilesInSet = 8\nMarbleMadness = 24\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; Marble Madness dirt road slopes\n;\n[TileSet0024]\nSetName = MM Dirt Road Slopes\nFileName = MDRSLP\nTilesInSet = 8\nNonMarbleMadness = 23\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nAllowTiberium = true\n\n;\n; Slope set pieces\n;\n[TileSet0025]\nSetName = Slope Set Pieces\nFileName = RAMP\nTilesInSet = 10\nMarbleMadness = 26\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nShadowCaster = true\nShadowTiles = 10\nRequiredForRMG = true\n\n;\n; Slope set pieces - Marble Madness version\n;\n[TileSet0026]\nSetName = Slope Set Pieces\nFileName = MRAM\nTilesInSet = 10\nNonMarbleMadness = 25\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\n;ShadowCaster = true\n;ShadowTiles = 10\n\n;\n; A dead oil tanker\n;\n[TileSet0027]\nSetName = Dead Oil Tanker\nFileName = TANKER\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; Some ruins\n;\n[TileSet0028]\nSetName = Ruins\nFileName = RUIN\nTilesInSet = 1\nAllowBurrowing=false\n\n;\n; Height pieces for marble madness mode\n;\n[TileSet0029]\nSetName = New MM Height Pieces\nFileName = hyte\nTilesInSet = 10\nMorphable = true\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nAllowToPlace=no\nAllowTiberium = true\n\n;\n; Waterfalls.\n;\n[TileSet0030]\nSetName = Waterfalls\nFileName = W-a-\nTilesInSet = 4\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nMarbleMadness=62\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0031]\nSetName = Ground 01\nFileName = Des01\nTilesInSet = 48\nLowRadarColor = 10,80,80\nHighRadarColor = 10,128,128\nAllowTiberium = true\n\n[TileSet0032]\nSetName = Ground 02\nFileName = Des02\nTilesInSet = 48\nLowRadarColor = 10,90,90\nHighRadarColor = 10,128,128\nAllowTiberium = true\n\n\n[TileSet0033]\nSetName = Sand\nFileName = Sandy\nTilesInSet = 1\nMorphable = true\nLowRadarColor = 10,90,90\nHighRadarColor = 10,128,128\nToSnowTheater=62\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; L.A.T. system for connecting sandy and normal terrain\n;\n[TileSet0034]\nSetName = Sand/Clear LAT\nFileName = dlat\nTilesInSet = 16\nMorphable = true\nLowRadarColor = 50,90,90\nHighRadarColor = 70,128,128\nAllowToPlace=no\nToSnowTheater=63\nRequiredForRMG = true\nAllowTiberium = true\n\n[TileSet0035]\nSetName = Rough ground\nFileName = Rough\nTilesInSet = 10\nMorphable = false\nToSnowTheater = 40\nLowRadarColor = 80,60,0\nHighRadarColor = 110,80,0\nRequiredForRMG = true\n\n[TileSet0036]\nSetName = Paved Road Ends\nFileName = p_end\nTilesInSet = 4\nToSnowTheater = 38\nMorphable = false\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0037]\nSetName = TrainBridges\nFileName = Tovrps\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\n\n[TileSet0038]\nSetName = Pavement\nFileName = Pave\nTilesInSet = 14\nMorphable = false\nLowRadarColor = 128,128,128\nHighRadarColor = 128,128,128\nAllowBurrowing=false\nRequiredForRMG = true\nToSnowTheater = 69\n;\n; L.A.T. system for connecting pavement and normal terrain\n;\n[TileSet0039]\nSetName = Pavement/Clear LAT\nFileName = plat\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 110,80,40\nHighRadarColor = 150,100,65\nAllowToPlace=no\nAllowBurrowing=false\nRequiredForRMG = true\nToSnowTheater=70\n\n[TileSet0040]\nSetName = Paved road bits\nFileName = proadc\nTilesInSet = 14\nMorphable = false\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0041]\nSetName = Green\nFileName = Green\nTilesInSet = 1\nMorphable = true\nLowRadarColor = 10,100,10\nHighRadarColor = 10,120,10\nToSnowTheater=64\nRequiredForRMG = true\nAllowTiberium = true\n\n;\n; L.A.T. system for connecting green and normal terrain\n;\n[TileSet0042]\nSetName = Green/Clear LAT\nFileName = glat\nTilesInSet = 16\nMorphable = true\nLowRadarColor = 40,90,0\nHighRadarColor = 80,110,0\nAllowToPlace=no\nToSnowTheater=65\nRequiredForRMG = true\nAllowTiberium = true\n\n[TileSet0043]\nSetName = Ramp edge fixup\nFileName = Rmpfx\nTilesInSet = 12\nMorphable = true\nMarbleMadness = 44\nToSnowTheater = 41\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nRequiredForRMG = true\nAllowTiberium = true\n\n\n[TileSet0044]\nSetName = Ramp edge fixup - Marble Madness\nFileName = Mrmfx\nTilesInSet = 12\nMorphable = true\nNonMarbleMadness = 43\nToSnowTheater = 42\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nAllowTiberium = true\n\n[TileSet0045]\nSetName = Water slopes\nFileName = WSLOPE\nTilesInSet = 4\nMorphable = no\nToSnowTheater = 43\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nMarbleMadness=68\nAllowBurrowing=false\n\n[TileSet0046]\nSetName = Pavement (Use for LAT)\nFileName = Pvclr\nTilesInSet=1\nMorphable=no\nLowRadarColor = 128,128,128\nHighRadarColor = 128,128,128\nAllowBurrowing=false\nRequiredForRMG = true\nToSnowTheater=68\n\n[TileSet0047]\nSetName = Paved Road Slopes\nFileName = Prslpe\nTilesInSet = 4\nMorphable = no\nToSnowTheater = 44\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\n\n[TileSet0048]\nSetName = Monorail Slopes\nFileName = Tslope\nTilesInSet = 4\nMorphable = no\nMarbleMadness = 85\nToSnowTheater = 45\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\n\n[TileSet0049]\nSetName = Waterfalls-B\nFileName = W-b-\nTilesInSet = 4\nToSnowTheater = 35\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nMarbleMadness=63\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0050]\nSetName = Waterfalls-C\nFileName = W-c-\nTilesInSet = 4\nToSnowTheater = 36\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nMarbleMadness=64\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0051]\nSetName = Waterfalls-D\nFileName = W-d-\nTilesInSet = 4\nToSnowTheater = 37\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nMarbleMadness=65\nAllowBurrowing=false\nRequiredForRMG = true\n\n[TileSet0052]\nSetName = Newest MM Height\nFileName = hyte\nTilesInSet = 15\nMorphable = true\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nAllowToPlace=no\nAllowBurrowing=false\nAllowTiberium = true\n\n[TileSet0053]\nSetName = Tunnel Floor\nFileName = tunnel\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToSnowTheater=47\nMarbleMadness=83\nAllowBurrowing=false\n\n[TileSet0054]\nSetName = Tunnel Side\nFileName = tunnex\nTilesInSet = 2\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nMarbleMadness=84\nToSnowTheater=48\nAllowBurrowing=false\n\n[TileSet0055]\nSetName = TrackTunnel Floor\nFileName = tunnet\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToSnowTheater=49\nMarbleMadness=83\nAllowBurrowing=false\n\n[TileSet0056]\nSetName = Destroyable Cliffs\nFileName = dcliff\nTilesInSet = 2\nMorphable = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nToSnowTheater=61\nMarbleMadness=86\nAllowBurrowing=false\n\n[TileSet0057]\nSetName = Water Caves\nFileName = Wcave\nTilesInSet = 8\nMorphable = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nMarbleMadness=66\nAllowBurrowing=false\n\n;\n; Dirt roads. Corner pieces. Marble Madness version.\n;\n[TileSet0058]\nSetName = MM Bendy Dirt Roads\nFileName = MDrodc\nTilesInSet = 24\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 16\nToSnowTheater = 50\nAllowTiberium = true\n\n;\n; Dirt roads. Junctions.  Marble Madness version.\n;\n[TileSet0059]\nSetName = MM Dirt Road Junctions\nFileName = MDrodj\nTilesInSet = 11\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 17\nToSnowTheater = 51\nAllowTiberium = true\n\n;\n; Dirt roads. Straights.  Marble Madness version.\n;\n[TileSet0060]\nSetName = MM Straight Dirt Roads\nFileName = MDrods\nTilesInSet = 66\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness = 18\nToSnowTheater = 52\nAllowTiberium = true\n\n;\n; Shore pieces. Marble madness version.\n;\n[TileSet0061]\nSetName = MM Shore Pieces\nFileName = MShore\nTilesInSet = 42\nLowRadarColor = 30,30,40\nHighRadarColor = 30,30,40\nNonMarbleMadness = 12\nAllowBurrowing=false\n\n;\n; Waterfalls. MM.\n;\n[TileSet0062]\nSetName = MM Waterfalls\nFileName = MWa-\nTilesInSet = 4\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nNonMarbleMadness=30\nToSnowTheater = 54\nAllowBurrowing=false\n\n[TileSet0063]\nSetName = MM Waterfalls-B\nFileName = MWb-\nTilesInSet = 4\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nNonMarbleMadness=49\nToSnowTheater = 55\nAllowBurrowing=false\n\n[TileSet0064]\nSetName = MM Waterfalls-C\nFileName = MWc-\nTilesInSet = 4\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nNonMarbleMadness=50\nToSnowTheater = 56\nAllowBurrowing=false\n\n[TileSet0065]\nSetName = MM Waterfalls-D\nFileName = MWd-\nTilesInSet = 4\nLowRadarColor = 128,128,192\nHighRadarColor = 192,192,255\nNonMarbleMadness=51\nToSnowTheater = 57\nAllowBurrowing=false\n\n[TileSet0066]\nSetName = MM Water Caves\nFileName = MWcave\nTilesInSet = 8\nMorphable = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nNonMarbleMadness=57\nAllowBurrowing=false\n\n;\n; MM Cliff pieces that meet water pieces\n;\n[TileSet0067]\nSetName = Cliff/Water pieces\nFileName = MWClif\nTilesInSet = 28\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nNonMarbleMadness=15\nToSnowTheater=58\nAllowBurrowing=false\n\n[TileSet0068]\nSetName = MM Water slopes\nFileName = MWSLOP\nTilesInSet = 4\nMorphable = no\nLowRadarColor = 110,80,0\nHighRadarColor = 150,110,0\nNonMarbleMadness=45\nToSnowTheater = 59\nAllowBurrowing=false\n\n;\n; Just icy water.\n;\n[TileSet0069]\nSetName = MM Water\nFileName = MWater\nTilesInSet = 14\nLowRadarColor = 10,10,30\nHighRadarColor = 10,10,50\nNonMarbleMadness=21\nToSnowTheater=60\nAllowBurrowing=false\n\n\n[TileSet0070]\nSetName = Scrin Wreckage\nFileName = Scrin\nTilesInSet = 6\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nMarbleMadness=71\nAllowBurrowing=false\n\n[TileSet0071]\nSetName = MM Scrin Wreckage\nFileName = MScrin\nTilesInSet = 6\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nNonMarbleMadness=70\nAllowBurrowing=false\n\n[TileSet0072]\nSetName = DirtTrackTunnel Floor\nFileName = dtunnt\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToSnowTheater=66\nMarbleMadness=83\nAllowBurrowing=false\n\n[TileSet0073]\nSetName = DirtTunnel Floor\nFileName = dtunn\nTilesInSet = 4\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nToSnowTheater=67\nMarbleMadness=83\nAllowBurrowing=false\n\n; Crystal terrain\n[TileSet0074]\nSetName = Crystal LAT tile\nFileName = Crys\nTilesInSet = 1\nMorphable = false\nLowRadarColor = 70,40,0\nHighRadarColor = 80,45,0\nRequiredForRMG = true\nAllowTiberium = false\n\n; L.A.T. system for connecting crystal and clear terrain\n[TileSet0075]\nSetName = Clear Crystal LAT\nFileName = Cylat\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 80,45,0\nHighRadarColor = 100,60,0\nAllowToPlace = no\nRequiredForRMG = true\nAllowTiberium = false\n\n; Swamp terrain\n[TileSet0076]\nSetName = Swampy\nFileName = Swamp\nTilesInSet = 9\nMorphable = false\nLowRadarColor = 70,40,0\nHighRadarColor = 80,45,0\nRequiredForRMG = true\nAllowTiberium = false\nAllowBurrowing=false\n\n; L.A.T. system for connecting swamp and water terrain\n[TileSet0077]\nSetName = Swampy LAT\nFileName = Slat\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 80,45,0\nHighRadarColor = 100,60,0\nAllowToPlace = no\nRequiredForRMG = true\nAllowTiberium = false\nAllowBurrowing=false\n\n; Blue Mold terrain\n[TileSet0078]\nSetName = Blue Mold\nFileName = Blue\nTilesInSet = 1\nMorphable = false\nLowRadarColor = 70,40,0\nHighRadarColor = 80,45,0\nRequiredForRMG = true\nAllowTiberium = false\nAllowBurrowing=false\n\n; L.A.T. system for connecting crystal and clear terrain\n[TileSet0079]\nSetName = Blue Mold LAT\nFileName = Blat\nTilesInSet = 16\nMorphable = false\nLowRadarColor = 80,45,0\nHighRadarColor = 100,60,0\nAllowToPlace = no\nRequiredForRMG = true\nAllowBurrowing=false\nAllowTiberium = false\n\n; Crystal Cliffs\n[TileSet0080]\nSetName = Crystal Cliff\nFileName = CCliff\nTilesInSet = 6\nMorphable = false\nShadowCaster = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nMarbleMadness=82\nAllowBurrowing=false\nRequiredForRMG = true\n\n; Kodiak Crash\n[TileSet0081]\nSetName = Kodiak Crash\nFileName = Crash\nTilesInSet = 7\nLowRadarColor = 60,40,0\nHighRadarColor = 80,50,0\nAllowBurrowing=false\nRequiredForRMG = false\n\n;\n;New MarbleMadness set. \n;\n; MM Crystal Cliff\n[TileSet0082]\nSetName = MM Crystal Cliff pieces\nFileName = MCClif\nTilesInSet = 6\nMorphable = false\nShadowCaster = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nNonMarbleMadness=80\nAllowBurrowing=false\n\n; MM Tunnel set\n[TileSet0083]\nSetName = MM Tunnel set\nFileName = mtunnl\nTilesInSet = 4\nShadowCaster = false\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nNonMarbleMadness=53\nAllowBurrowing=false\n\n[TileSet0084]\nSetName = MM Tunnel Side\nFileName = mtunnx\nTilesInSet = 2\nMorphable = false\nLowRadarColor=150,150,192\nHighRadarColor=200,200,255\nNonMarbleMadness=53\nToSnowTheater=48\n\n[TileSet0085]\nSetName = MM Monorail Slopes\nFileName = Mtslop\nTilesInSet = 4\nMorphable = no\nNonMarbleMadness = 48\nToSnowTheater = 45\nLowRadarColor = 92,92,92\nHighRadarColor = 92,92,92\nAllowBurrowing=false\n\n[TileSet0086]\nSetName = MM Destroyable Cliffs\nFileName = Mdclif\nTilesInSet = 2\nMorphable = false\nLowRadarColor = 90,65,0\nHighRadarColor = 110,80,0\nToSnowTheater=61\nNonMarbleMadness=56\nAllowBurrowing=false\n\n\n;\n; Animating tiles\n;\n[Tunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[TrackTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[DirtTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n[DirtTrackTunnel Floor]\nTile01Anim=TUNTOP01\nTile01XOffset=-48\nTile01YOffset=-37\nTile01AttachesTo=2\nTile01ZAdjust=-10\nTile02Anim=TUNTOP02\nTile02XOffset=48\nTile02YOffset=-37\nTile02AttachesTo=10\nTile02ZAdjust=-10\nTile03Anim=TUNTOP03\nTile03XOffset=-2\nTile03YOffset=-13\nTile03AttachesTo=0\nTile03ZAdjust=-100\nTile04Anim=TUNTOP04\nTile04XOffset=0\nTile04YOffset=-13\nTile04AttachesTo=0\nTile04ZAdjust=-100\n\n;[Scrin Wreckage]\n;Tile05Anim=UFO\n;Tile05XOffset=12\n;Tile05YOffset=-10\n;Tile05AttachesTo=3\n;Tile05ZAdjust=-100\n\n\n[Waterfalls]\nTile01Anim=WA01X\nTile01XOffset=-9\nTile01YOffset=54\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WA02X\nTile02XOffset=-39\nTile02YOffset=39\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WA03X\nTile03XOffset=-26\nTile03YOffset=47\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WA04X\nTile04XOffset=-38\nTile04YOffset=37\nTile04AttachesTo=0\nTile04ZAdjust=0\n\n[Waterfalls-B]\nTile01Anim=WB01X\nTile01XOffset=30\nTile01YOffset=43\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WB02X\nTile02XOffset=43\nTile02YOffset=38\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WB03X\nTile03XOffset=29\nTile03YOffset=49\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WB04X\nTile04XOffset=9\nTile04YOffset=56\nTile04AttachesTo=0\nTile04ZAdjust=0\n\n[Waterfalls-C]\nTile01Anim=WC01X\nTile01XOffset=-2\nTile01YOffset=19\nTile01AttachesTo=0\nTile01ZAdjust=0\nTile02Anim=WC02X\nTile02XOffset=5\nTile02YOffset=-6\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WC03X\nTile03XOffset=14\nTile03YOffset=1\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WC04X\nTile04XOffset=-41\nTile04YOffset=-5\nTile04AttachesTo=1\nTile04ZAdjust=0\n\n[Waterfalls-D]\nTile01Anim=WD01X\nTile01XOffset=-8\nTile01YOffset=-4\nTile01AttachesTo=1\nTile01ZAdjust=0\nTile02Anim=WD02X\nTile02XOffset=-2\nTile02YOffset=-9\nTile02AttachesTo=0\nTile02ZAdjust=0\nTile03Anim=WD03X\nTile03XOffset=-17\nTile03YOffset=-2\nTile03AttachesTo=0\nTile03ZAdjust=0\nTile04Anim=WD04X\nTile04XOffset=2\nTile04YOffset=20\nTile04AttachesTo=0\nTile04ZAdjust=0\n"
  },
  {
    "path": "DXMainClient/Resources/INI/theme.ini",
    "content": "; THEME.INI\n; Lists and controls the musical themes available in the game.\n\n; ******* Theme Controls *******\n; Each theme is listed here. Even if the theme is not normally available\n; in the play list, it still must be declared.\n[Themes]\n00=INTRO\t;Main Menu Theme\n01=VALVES1B\t;Valves\n02=DUSKHOUR\t;Dusk Hour\n03=FLURRY\t;Flurry\n04=MUTANTS\t;Mutants\n05=APPROACH\t;Approach\n06=GLOOM\t;Gloom\n07=INFRARED\t;Infrared\n08=MADRAP\t;Mad Rap\n09=REDSKY\t;Red Sky\n10=STORM\t;Ion Storm\n11=TIMEBOMB\t;Time Bomb\n12=WHATLURK\t;What Lurks\n13=DEFENSE\t;Defense\n14=HEROISM\t;Heroism\n15=LONETROP\t;Lone Troop\n16=NODCRUSH\t;Nod Crush\n17=PHAROTEK\t;Pharotek\n18=SCOUT\t;Scout\n19=SCORE\t;Score Screen\n20=MAPS\t\t;Map Selection\n21=IONSTORM\t;Ion Storm Ambient\n22=FSMAP\t;FS Map Selection\n23=ELUSIVE\t;Elusive\n24=HACKER\t;Hacker\n25=INFILTRA\t;Infiltration\n26=LINKUP\t;Link Up\n27=KMACHINE\t;Killing Machine\n28=RAINNITE\t;Rain in the night (Part 2)\n29=SLAVESYS\t;Slave to the system\n30=FSMENU\t;FS Main Menu Theme\n31=DMACHINE\t;Deploy Machines\n\n\n; ******* Individual Theme Data *******\n; The following sections supply the information specific to\n; each theme declared.\n;\n; Name = display name of the theme\n; Length = length of the theme (in minutes)\n; Normal = Is it available through the in-game theme play list (def=yes)?\n; Scenario = the scenario when the theme becomes available (def=0)\n; Side = which side [or sides] get to hear this theme\n; Repeat = Does this theme always loop (def=no)?\n\n[INTRO]\nName=Intro\nLength=3.27\nNormal=no\nRepeat=yes\n\n[VALVES1B]\nName=Valves\nLength=3.27\nScenario=1\n\n[DUSKHOUR]\nName=Dusk Hour\nLength=4.11\nScenario=1\nSide=GDI\n\n[FLURRY]\nName=Flurry\nLength=4.11\nScenario=1\n\n[MUTANTS]\nName=Mutants\nLength=4.11\nScenario=1\nSide=GDI\n\n[APPROACH]\nName=Approach\nLength=4.42\nScenario=1\n\n[GLOOM]\nName=Gloom\nLength=3.37\nScenario=1\n\n[INFRARED]\nName=Infrared\nLength=4.26\nScenario=1\n\n[MADRAP]\nName=Mad Rap\nLength=4.29\nScenario=1\n\n[REDSKY]\nName=Red Sky\nLength=2.22\nScenario=1\n\n[STORM]\nName=Ion Storm\nLength=4.14\nScenario=1\n\n[TIMEBOMB]\nName=Time Bomb\nLength=2.04\nScenario=1\n\n[WHATLURK]\nName=What Lurks\nLength=5.13\nScenario=1\n\n[DEFENSE]\nName=Defense\nLength=4.03\nScenario=1\nSide=Nod\n\n[HEROISM]\nName=Heroism\nLength=4.06\nScenario=1\n\n[LONETROP]\nName=Lone Troop\nLength=4.39\nScenario=1\nSide=GDI\n\n[NODCRUSH]\nName=Nod Crush\nLength=3.45\nScenario=1\nSide=Nod\n\n[PHAROTEK]\nName=Pharotek\nLength=4.38\nScenario=1\nSide=Nod\n\n[SCOUT]\nName=Scout\nLength=4.14\nScenario=1\n\n[SCORE]\nName=Score Screen\nNormal=no\nRepeat=yes\n\n[MAPS]\nName=Map Selection\nNormal=no\nRepeat=yes\n\n[IONSTORM]\nName=Ion Storm Ambient\nNormal=no\nRepeat=yes\n\n[FSMAP]\nName=FS Map Selection\nNormal=no\nRepeat=yes\n\n[ELUSIVE]\nName=Elusive\nLength=4.27\nScenario=1\n\n[HACKER]\nName=Hacker\nLength=4.02\nScenario=1\n\n[INFILTRA]\nName=Infiltration\nLength=4.20\nScenario=1\n\n[LINKUP]\nName=Link Up\nLength=3.17\nScenario=1\n\n[KMACHINE]\nName=Killing Machine\nLength=3.27\nScenario=1\n\n[RAINNITE]\nName=Rain in the night (Part 2)\nLength=3.59\nScenario=1\n\n[SLAVESYS]\nName=Slave to the system\nLength=2.36\nScenario=1\n\n[FSMENU]\nName=FS Menu\nNormal=no\nRepeat=yes\n\n[DMACHINE]\nName=Deploy Machines\nLength=3.42\nScenario=1\n\n"
  },
  {
    "path": "DXMainClient/Resources/INI/tutorial.ini",
    "content": ";===============================================\n; Tutorial Text for Tiberian Sun and Firestorm\n;===============================================\n;\n;Generic Text\n;------------\n;640x400 Max line length is...\n;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n;640x480 Max line length is...\n;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n;800x600 Max line length is...\n;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n;-------------------------\n; Tiberian Sun Tutorial\n;-------------------------\n[Tutorial]\n1=Harvest the Tiberium to the north.\n2=Objective One: Destroy all of Hassan's elite guard.\n3=Objective One: Capture Hassan's T.V. station to the east.\n4=Objective One: Build a Tiberium Refinery and begin harvesting the Tiberium to the southeast.\n5=Objective Two: Build a Barracks to create more infantry.\n6=Objective Three: Destroy all Nod forces in the area.\n7=Note that your power is getting low. To get more power, build more Power Plants.\n8=If your Tiberium Refinery is full, build Tiberium Silos to store the excess Tiberium.\n9=Objective Two: To get production online build a Tiberium Refinery.\n10=Beware Tiberium is lethal to unprotected infantry.\n11=Stand forward and be recognized!\n12=Stand and Identify yourself in the name of Kane.\n13=Sound the Alarm! Slavik's Forces are here! \n14=Hassan Soldier: Hold them here!\n15=The traitors are coming, destroy the bridge!\n16=Objective Two: Destroy Hassan's elite guard base.\n17=We've been touched by the spirit hand of Kane, and are ready to serve the technology of peace. \n18=Down with Hassan!!!\n19=Build more infantry to defend the base!\n20=****ESTABLISHING BATTLEFIELD CONTROL****..........Standby!\n21=Power levels are low. Construct more Power Plants.\n22=****BATTLEFIELD CONTROL ESTABLISHED****\n23=To repair a bridge, send an engineer into the repair hut near the bridge base.\n24=The temple has been discovered, NOW DESTROY the GDI trespassers.\n25=Objective One: Locate the crashed UFO and retrieve Kane's artifacts from inside. \n26=Sir! The Tacitus is gone. Vega's men must've grabbed it.\n27=Objective Two: Retrieve the cargo from the train.\n28=Pull over for inspection!\n29=O.K.! You're clear to enter. \n30=Nod ICBMs detected. To stop them, DESTROY the launchers.\n31=Clear the zone for M.C.V. dropship deployment!\n32=Base perimeter has been breached!\n33=To repair a structure left-click on the wrench icon in the sidebar and left-click on the structure.\n34=Laser Turrets! RUN FOR IT!\n35=Objective One: Contact the mutants - try searching near the local hospital.\n36=Objective Two: Clear both ends of the tunnel to the west.\n37=Objective Three: Locate the research facility to the north.\n38=Objective Four: Destroy the research facility.\n39=Objective One: Locate the toxin trucks.\n40=Objective Two: Escort the toxin trucks past the GDI checkpoint to the east.\n41=Objective One: Locate the abandoned Nod base to the north.\n42=Objective Two: Destroy the GDI base.\n43=Use the Weed Eater units to harvest the Tiberium veins.\n44=Objective One: Establish a base and build a Tiberium Waste Facility.\n45=Objective Two: Destroy the GDI base.\n46=Tiberium waste convoy approaching.\n47=Convoy destroyed.\n48=Firestorm perimeter deactivated.\n49=Objective One: Deploy the ICBM launchers near the beacons.\n50=First launcher deployed.\n51=Second launcher deployed.\n52=Third launcher deployed. Objective complete.\n53=The Philadelphia has been destroyed!\n54=Objective Two: Build the Temple of Nod.\n55=Launcher destroyed.\n56=Mission failed.\n57=Objective One: Infiltrate the GDI Communication Upgrade Centers.\n58=Codes located.\n59=Proceed to the next Communication Upgrade Center.\n60=Objective Two: Evacuate the Chameleon Spy.\n61=Supplies found.\n62=Objective One: Establish a base.\n63=Deploy the M.C.V. by double left clicking on the M.C.V.\n64=Objective Two: Destroy all five Nod SAM sites.\n65=Objective Reached: Civilians evacuated.\n66=Objective Three: Destroy all Nod forces.\n67=Objective One: Locate and secure the crash site.\n68=Objective Two: Capture Nod Technology Center.\n69=Objective Reached: Site secure\n70=Objective Reached: Technology Center captured.\n71=Warning: Mission critical unit under attack.\n72=Warning: Mission critical structure under attack.\n73=Objective One: Locate and free the mutants.\n74=Objective Two: Evacuate the mutants.\n75=Objective Reached: Mutants freed.\n76=Objective One: Destroy the supply base.\n77=Objective One: Plant C4 on all ten Nod power plants.\n78=Objective One: Destroy Nod missile complex.\n79=Detonate C4 when ready.\n80=Stop! Don't Shoot! I was forced to work for them.\n81=Take out this sentry post and I will show you their nearby base.\n82=CABAL: Hassan's Base has been alerted. Attack is imminent.\n83=Now get an engineer over here to fix this bridge and I \n84=will alert Hassan to their presence.\n85=CABAL: Establish a foothold on the far side of this bridge\n86=and an MCV will be sent in to you.\n87=CABAL: MCV has arrived to the southeast.\n88=To build or train left-click on the icons located in the sidebar.\n89=To deploy a vehicle select it, place the cursor over the vehicle and left-click on it.\n90=GDI has detected you.\n91=The Temple is under attack!\n92=Mutant vermin detected.\n93=Tiberium lifeform detected.\n94=Objective One: Locate the old Temple of Nod.\n95=Objective Two: Remove the GDI trespassers.\n96=Tacitus has been acquired.\n97=GDI dropship detected.\n98=Bullet train departing.\n99=Prevent the train from departing and retrieve the Tacitus.\n100=GDI bullet train arriving.\n101=GDI bullet train departing.\n102=Transport has arrived.\n103=Transport lost.\n104=Objective One: Find and rescue Oxanna.\n105=Objective Two: Commandeer a transport to escape.\n106=CAUTION: THIN ICE.\n107=Kodiak under attack!\n108=Storm abating. Commence attack on Nod forces.\n109=Kodiak destroyed!\n110=Kodiak in critical condition!\n111=Eye of the storm has been entered. \n112=Maximum efficiency for equipment can now be achieved.\n113=Reentering ion storm, caution is advised.\n114=Objective One: Protect the Kodiak at all costs.\n115=Objective Two: Destroy all Nod forces.\n116=Philadelphia in range.\n117=ICBM launch detected.\n118=Tiberium Missile launched.\n119=ICBM destroyed!\n120=Objective One: Stop the launch of the Tiberium Missile.\n121=Objective Two: Destroy the ICBMs targeted at the Philadelphia.\n122=ICBMs destroyed! Philadelphia out of danger.\n123=Proceed with Tiberium Missile destruction.\n124=Objective Three: Destroy all Nod forces.\n125=Civilian city is under attack!\n126=PEACE THROUGH POWER!\n127=****BROADCASTING****\n128=This map is under redesign.\n129=GDI Soldier: Shit, We're outnumbered! Return to base now and alert them.\n130=CABAL: During the Ion Storm their Radar/Communications will be down. Now is the opportune time to hit them before the \n131=storm abates.\n132=CABAL: GDI Communications have been reestablished. \n133=They are sending a transmission to Sarajevo now.\n134=Your venture has been quite unsuccessful, to state the least.\n135=Move quickly, before they see us.\n136=We have to get this to Tratos immediately.\n137=Holy $#!+ its Nod! I have to warn the base.\n138=What's the E.T.A. on that M.C.V.? This UFO gives me the creeps.\n139=Looks like they're going to ship it out via bullet train.\n140=Current weapon range insufficient. Weapon drop in progress.\n141=Thanks for the help!\n142=Power overload in progress...\n143=GDI forces spotted. Blow the bridge!\n144=Objective one: Destroy all Nod structures.\n145=Objective two: Capture the train station. DO NOT DESTROY IT!\n146=Objective one complete.\n147=Nod base is heavily guarded by lasers. Suggestion: destroying power plants to west may cause overload.\n148=Objective two complete.\n149=Umagon: My people are nearby.\n150=We will help.\n151=Alert! GDI presence detected!\n152=Perimeter secure. Deactivating alarm.\n153=Alert! Prison break in progress!\n154=Orca Transport: Negative on extraction until SAM sites are eliminated!\n155=GDI Forces Spotted! Falling back to alert base.\n156=Objective: Rescue captives from the prison to the east.\n157=Objective one: Spy on GDI comm center to learn the location of the weapons test.\n158=Objective two: Destroy the Mammoth Mark II prototype.\n159=GDI: The MM2 is quite effective against structures.\n160=GDI: Now watch the effectiveness against ground units.\n161=GDI: The MM2 is equally deadly to air-based assaults.\n162=GDI: This concludes the Mammoth Mark II demonstration.\n163=We have Hassan pinned and ready to be brought in Commander Slavick. Orders are complete.\n164=Sir! I believe there is an old GDI base near. It could be worth looking into.\n165=We should rendezvous with the rescue team to the south.\n166=UFO crash sight located.\n167=Hey... over here! Help... Destroy these trucks to free us.\n168=Captured Commander: All right! Now get me to your drop-off site and into the evac unit.\n169=STOP THAT TRAIN!\n170=Ghost Stalker: If you can get me onto that train, we can do some real damage!\n171=Mutants: The charges are placed. We can get the laser wall down in 30 minutes.\n172=Mutants: The wall is down - you are clear to attack!\n173=Objective one: Destroy all the chemical tanks.\n174=Objective two: Destroy the Nod base.\n175=Objective one: Destroy all chemical missile launch sites.\n176=New secondary objective: Destroy primary AND secondary Nod bases.\n177=Oxanna located.\n178=GDI: Jake, it's a trap! Get to the airbase!\n179=GDI: Jake, the transport will take 30 minutes to arrive. Hold on!\n180=GDI: Patrol to base! Nod troops in area! Abort tour!\n181=New Objective: Get Ghost Stalker onto the train. Ghost must not die!\n182=Nod: Commander, you have been provided with a direct satellite uplink for this mission. \n183=Nod: Look to your radar now and you will see the three locations of the mobile sensor arrays.\n184=You have been provided with 2 Artillery units. Good hunting, reinforcements will be arriving soon...\n185=Oxanna is being moved to the main GDI base.\n186=Nod: Umagon has escaped. Your mission has failed.\n187=Nod: Umagon has been detected in the northeast quadrant. \n188=She is boarding a train bound for the GDI base in the south.\n189=Nod: Umagon has reached the GDI base and is moving to board the train leaving this region.\n190=GDI: We've lost the beacon. Extraction time will be delayed 15 minutes.\n191=Objective One: Capture the GDI base before McNeil arrives.\n192=Objective Two: Use Toxin Soldiers to \"convince\" McNeil to join us.\n193=Objective Three: Get McNeil into the APC at the extraction point.\n194=Our cover is blown! Capture McNeil by any means possible!\n195=GDI is going after our extraction APC It must not be destroyed!\n196=Thanks! We can use the supplies.  I'll go gather my people.\n197=Nod: All sensor arrays are down. Full area map generation downloading now.\n198=Special objective complete.\n199=Nod:  We can use these old units to our advantage.  Rerouting their control to you in 3,\n200=2,\n201=1...\n202=Hey! Where'd all those shiners come from?\n203=Nod: Umagon's dropship transport has been located and will arrive in 10 minutes.\n204=Nod: Umagon's dropship transport will arrive in 5 minutes.\n205=Nod: Umagon's dropship transport will arrive in 1 minute.\n206=Nod: Umagon's dropship transport has arrived and she is moving to board the southern train.\n207=Nod: Umagon is moving to board the northern train which leaves the region. Her escape is imminent.\n208=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.\n209=GDI: Hurry Jake! They're right behind you!\n210=GDI: Jake, it's good to see...Hey! What are you doing?\n211=Two launchers remaining.\n212=One launcher remaining.\n213=Mutants: Liars! GDI is trying to help us! You will die for this!\n214=Umagon: My people are waiting somewhere to the north.\n215= SCROOGE!\n216=Objective One: Capture the remaining GDI structures within this base to build a force to capture Tratos.\n217=Objective Two: Now find the Mutant Headquarters and knock on their door (attack it!). This should convince Tratos to be sympathetic to our cause.\n218=The Philadelphia is passing within ICBM Range.\n219=The Philadelphia has left ICBM range.\n220=Destroy the 7 SAM sites on the ridge to clear the way for our dropships.\n221=Cabal: General Vega, the secondary generators will come online in 20 minutes.\n222=Cabal: General Vega, the generators are online.  SAM sites active.\n223=EVA: We are currently tracking the Nod train carrying the target cargo.  Intel states that the bridge is out \n224=and we may hit the train before they repair the bridge.\n225=EVA: Alert! The bridge has been fixed and the Nod train is moving to its final destination within the base  \n226=to the South. Penetrate the bases defenses and retrieve that cargo.\n227=Objective one: Remove all Nod presence from the area.\n228=Objective two: Capture Vega's Pyramid.\n229=EVA: GDI reinforcements have arrived. Mammoth Mk II en route.  Estimated ETA in 2 minutes...\n230=EVA: Mammoth Mk II has arrived.\n231=CABAL: Philadelphia orbit tracking commencing!\n232=Mutants: Hold a moment, while their fighters pass by.\n233=Mutants: Okay, Go now.\n234=Mutants: The production facility has been located. Send in the reinforcements and let's finish this.\n235=Mutants: Damn, their base has been cloaked. We must wait for them to uncloak it.\n236=EVA: The cargo car of that train contains the crate of crystals that you are to recover.\n237=EVA: The bridge has been repaired and the train is making it's way to the Nod base in the south.\n238=EVA: Penetrate their base, destroy that cargo car and retrieve the crate holding the crystals.\n239=EVA: Umagon lost, mission failed.\n240=EVA: Ghost stalker lost, mission failed.\n241=EVA: Mcneil lost, mission failed.\n242=CABAL: Slavik lost, mission failed.\n243=EVA: The crystals have been retrieved, mission complete.\n244=CABAL: With the train destroyed Umagon will be stranded.  Find her and capture her.\n245=Tratos: Fight them my children, for the fate of our people.\n246=Tratos: You have killed enough of my children, take me and be done with this violence.\n247=Solomon: Change of plans - We have verified Vega's presence in the pyramid. CAPTURE the pyramid with Vega alive. DO NOT DESTROY IT!\n\n;----------------------\n; Firestorm Tutorial\n;----------------------\n248=A piece of CABAL's core has been recovered.\n249=GDI has detected your presence.\n250=GDI forces are near!\n251=First Objective: Get an Engineer into the Temple of Nod to retrieve part of CABAL's core.\n252=Second Objective: Retrieve a section of CABAL's core from the storage yard.\n253=Third Objective: Prevent transportation of the last section of the core.\n254=GDI is moving a piece of the core.\n255=Intercept the core piece before it is transported.\n256=Stop the cargo truck and retrieve the core piece.\n257=Get to the pick up zone for immediate evacuation!\n258= \n259=First Objective: Remain hidden from the GDI forces in the area.\n260=Second Objective: \"Persuade\" the civilians to assist our goal with the Toxin Soldiers.\n261=Third Objective: Destroy all GDI and civilian structures in the region.\n262=Use the Toxin soldiers to capture civilians.\n263=To bait the Tiberium life forms, lure them out with the drugged civilians.\n264=Lead the Tiberium life forms to the GDI/civilian occupied area.\n265=Once the life form has snacked on the civilian pawns it will feast on the settlements.\n266=First Objective: Find and evacuate any civilians in the area.\n267=Second Objective: Maintain ALL factories until reinforcements arrive.\n268=Escort the civilians to the ORCA transports for an immediate airlift.\n271=Mutant Guard: Halt, and prepare for vehicle inspection!\n272=Mutant Guard: Okay, looks good.  Head on in.\n273=CABAL: SAM sites destroyed.  Air power incoming.\n274=CABAL: You have been detected, Tratos is escaping by air transport.\n275=CABAL: You have failed, Tratos has escaped.\n276=CABAL: Arrays have been destroyed, sensors are now down.\n277=First Objective: Find the Kodiak.\n278=Second Objective: Determine if the Kodiak can be salvaged.\n279=Third Objective: Return the Tacitus to the drop zone beacon.\n280=EVA: Nod has acquired the Tacitus.  Recover it and return it to the drop zone.\n281=First Objective: Neutralize (do not kill) the four riot leaders.\n282=Second Objective: Protect food and water processors at all costs.\n283=Warning: Do not kill civilians or mutants!\n284=Warning: Prevent the destruction of mutant and civilian structures.\n285=First Objective: Get to the GDI outpost.\n286=Second Objective: Get Dr. Boudreau to the landing pad.\n287=Third Objective: Destroy all of CABAL's forces.\n288=First Objective: Locate the Mutant base.\n289=Second Objective: Return the truck containing the Tacitus to the beacon.\n290=Third Objective: Destroy all Mutant forces.\n291=Mutant: We've got you now Nod scum!\n292=First Objective: Locate the abandoned airfield.\n293=Second Objective: Repair the array.\n294=Third Objective: Retreat to the Montauk.\n295=Mutant Guard: It's bugged... Destroy it now!\n296=Tacitus recovered and loaded on the truck. Proceed to the beacon.\n297=Use the truck to transport the Tacitus when you locate it.\n298=Thanks! We saw something crash to the east. We also spotted a Nod MCV.  Be careful.\n299=CABAL: The capture of 6 power plants will shut down the Firestorm Generator.\n300=GDI Soldier: Sir, these laser posts are stronger than normal.\n301=EVA: Locating fence technicians may help in shutting down the laser fencing. Their probable location is within a civilian outpost to the north.\n302=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.\n303=EVA: Enemy Bridges may allow for unit reinforcement. Their destruction would be beneficial to this mission.\n304=CABAL on-line...\n305=EVA: Proceed to the base and secure it from attack.\n306=Riot leader neutralized.\n307=The serum within the tranquilizers they use will make them more accommodating to our plans.\n308=The life forms are located in a tiberium accelerated staging area called the Genesis Pit.\n309=CIVILIAN CASUALTY TOO HIGH!\n310=The orders were clear commander - NO DEATHS!\n311=Stop the patrol from alerting the base!\n312=Use the riot troops to force civilians and mutants to retreat.\n313=Use the Mobile EM-Pulse tanks to stop any mutant vehicles.\n314=Too many mutant structures have been lost.\n315=Too many civilian structures have been lost.\n316=Cyborg Commando online. Retaliation protocols initiated.\n317=Nod: CABAL forces are attacking! Evacuate the base!\n318=Zealot: Look! A Mutant!\n319=Zealot: Mutant Abomination! How dare you defile the\n320=        sanctity of our Holy ground?\n321=Priest: Kill the Mutant!\n322=Priest: STOP! THIEF!\n323=Zealot: Kill the Heretics!\n324=Zealot: Wha?! They've killed the Leader!\n325=Zealot: Existence is futile!\n326=Zealot: I'm coming to join you!\n327=Welcome stranger! Surely the divine one\n328=has guided your footsteps to this Holy Land.\n329=Can I offer you a tasty beverage?\n330=\"Temple of the Tacitus\"\n331=Archaeologist: Command, this is Valdez, I've got the Tacitus!\n332=GDI Command: Roger that Valdez, Transport is dusting \n333=off now.  Extraction in T minus two minutes.\n334=Join us!\n335=\"Temple of Time\"\n336=Cult Member: Welcome Traveler! Have you come to\n337=rejoice in the glory of our savior...\n338=Jebediah Smith?\n339=GDI Commander: This must be the base...\n340=GDI Commander: What the...!?\n341=GDI Commander: My God! CABAL is taking prisoners?\n342=This can't be good.\n343=GDI Commander: Arm Yourselves! CABAL is conscripting\n344=humans into his Cyborg army.\n345=Villager: Thanks for the warning.  Here's\n346=a reward for your concern.\n347=GDI Commander: Be warned! CABAL has set up\n348=operations in this sector and is capturing\n349=humans to turn them into Cyborgs.\n350=Villager: My God! Men arm yourselves!\n351=Women and children to the shelter!\n352=Here Commander, please take these two DISRUPTERS\n353=to help in your battle.\n354=GDI Commander: Attention Mutants! CABAL is \n355=currently harvesting biological components\n356=for his Cyborgs. Arm Yourselves!\n357=Mutant: Understood Blunt!  Take this\n358=HARVESTER for your troubles.\n359=GDI Commander: People of Trondheim, you must\n360=evacuate the city immediately!  CABAL is\n361=actively capturing civilians to turn them \n362=into Cyborgs.\n363=Mayor: Yes, yes. I will see to it that\n364=everyone is evacuated.  Please take this\n365=MCV and good luck.\n366=Civilian: MAYDAY! MAYDAY! We are currently under siege.\n367=Can anyone help us?\n368=Nod General: Commander, GDI command has requested\n369=that we aid these Civilians.  We cannot refuse.\n370=Rescue the plebes then take care of those harvesters.\n371=Nod General: Forget the Civilians, they are\n372=all dead.  Concentrate on those harvesters.\n373=Nod General: Well done, Commander.  GDI Command\n374=is so pleased that they have consented to send\n375=you a MCV.\n376=Nod Soldier: This should be easy enough.\n377=Nod Soldier: Let's get those harvesters!\n378=Archaeologist: The Hieroglyphs on this temple read:\n379=Priest: Kill the MUTANT invader!\n380=\"Temple of Thunder\"\n381=Use the ARCHAEOLOGIST to retrieve the Tacitus.\n382=ARCHAEOLOGIST killed!\n383=Get your people to the evacuation zone.\n384=Aircraft approaching.\n385=Priest: Praise this plant!\n386=Priest: Mortimer predicted the coming of these creatures.\n387=\n388=Priest: Blessed are the beasts!\n389=First Objective: Use your commander to warn the \n390=CIVILIANS.\n391=Second Objective: Destroy CABAL's base.\n392=Use your ENGINEER to steal an EVA unit from GDI's RADAR FACILITY.\n393=First Objective: Destroy all of CABAL's HARVESTERS, \n394=REFINERIES, and SILOS.\n395=Second Objective: Rescue the CIVILIANS.\n396=GDI Command: We are providing you with a new unit,\n397=the JUGGERNAUT. Do not waste it.\n398=Get your ENGINEER to the evacuation zone.\n399=Nod General: Well done, Commander.  GDI Command\n400=is so pleased that they have consented to send\n401=you more funding.\n402=Nod General: Well done, Commander.  Reinforcements\n403=en route.\n404=I am the power and the glory!\n405=Civilian: HELP! We are under attack!\n406=Montauk Destroyed!\n407=First Objective: Get the infected Cyborg into the communications network.\n408=Second Objective: Destroy the cyborg production plant.\n409=First Objective: Attach limpet mines to GDI units to penetrate the base and locate Tratos.\n410=Second Objective: Deactivate the firestorm defenses and neutralize the sensor arrays.\n411=Third Objective: Assassinate Tratos.\n412=Cabal: Power plant eliminated. 5 left to capture or destroy.\n413=Cabal: Power plant eliminated. 4 left to capture or destroy.\n414=Cabal: Power plant eliminated. 3 left to capture or destroy.\n415=Cabal: Power plant eliminated. 2 left to capture or destroy.\n416=Cabal: Power plant eliminated. 1 left to capture or destroy.\n417=To gain access to the Genesis Pit repair the bridge to the north of your location.\n418=Repair the bridge to gain access to the Genesis Pit.\n\n; Demo maps tutorial\n\n419=Click on your units to select them.\n420=Once selected, left-click where you want the unit(s) to move.\n421=You can unselect the selected unit(s) by right-clicking.\n422=As you move, you reveal terrain, structures, and enemy units.\n423=To attack, select your unit(s), then left-click on the enemy unit or structure.\n424=If you left-click and hold, you can make a band box to select many units at once.\n425=Explore the city and destroy all Nod units.\n426=Tiberium is a strange mineral that is collected and refined for money.\n427=Destroy this Nod outpost to win the mission.\n428=Every unit has different abilities. These MLRS can cross water.\n429=Crates contain power-ups and other bonuses.\n430=Bridges can be destroyed and repaired.\n431=To repair a bridge select an engineer, and send it into the bridge hut.\n432=Excellent work, Commander!\n433=GDI has already established the basics of your base for you.\n434=To build structures or units, left-click on their pictures on the sidebar.\n435=A barracks will allow you to train infantry-type units.\n436=Power plants will increase the amount of power in your base.\n437=A Tiberium Refinery allows you to collect Tiberium and make more money.\n438=The War Factory lets you produce vehicle-type units.\n439=Radar allows you to see a map of the battlefield, including enemy objects.\n440=When a structure is shows READY, click on it, then place it on the map.\n441=To make money, build a Tiberium refinery.\n442=The harvester will automatically harvest the nearest patch of Tiberium.\n443=To keep power up, build more power plants.\n444=Find the Nod base, and destroy all structures to win.\n445=Silos hold extra money so you don't lose any when the refinery is full.\n446=Vulcan cannons must be placed on empty component towers to work.\n447=Component towers are used to place fixed defenses in your base.\n448=Veinhole monsters can attack any non-infantry unit that touches their veins.\n449=Tiberium damages infantry-type units as they cross it. Keep them away if possible.\n450=Some cliffs are destroyable. Attack them to create a ramp.\n451=To repair, left-click on the repair wrench then the damaged structure.\n\n"
  },
  {
    "path": "DXMainClient/Resources/Map Editor/test.txt",
    "content": ""
  },
  {
    "path": "DXMainClient/Resources/Maps/Custom/custom maps.txt",
    "content": "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."
  },
  {
    "path": "DXMainClient/Resources/SUN.ini",
    "content": "[Audio]\nPlayMainMenuMusic=False\n\n[Options]\nIsFirstRun=False\nCheckforUpdates=False\nWriteInstallationPathToRegistry=False\nPrivacyPolicyAccepted=True\n\n[MultiPlayer]\nDiscordIntegration=False\n\n[Video]\nClientResolutionX=1280\nClientResolutionY=720\nBorderlessWindowedClient=False\nIntegerScaledClient=True\n\n"
  },
  {
    "path": "DXMainClient/Startup.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Threading;\nusing Microsoft.Win32;\nusing DTAClient.Domain;\nusing ClientCore;\nusing Rampastring.Tools;\nusing DTAClient.DXGUI;\nusing ClientUpdater;\nusing System.Security.Principal;\nusing System.DirectoryServices;\nusing System.Linq;\nusing DTAClient.Online;\nusing ClientCore.INIProcessing;\nusing ClientCore.Enums;\nusing System.Threading.Tasks;\nusing System.Globalization;\nusing System.Management;\nusing System.Runtime.InteropServices;\nusing System.Runtime.Versioning;\nusing ClientCore.Settings;\nusing ClientGUI;\nusing Steamworks;\n\nnamespace DTAClient\n{\n    /// <summary>\n    /// A class that handles initialization of the Client.\n    /// </summary>\n    public class Startup\n    {\n        /// <summary>\n        /// The main method for startup and initialization.\n        /// </summary>\n        public void Execute()\n        {\n            ProgramConstants.RESOURCES_DIR = SafePath.CombineDirectoryPath(ProgramConstants.BASE_RESOURCE_PATH, UserINISettings.Instance.ThemeFolderPath);\n\n            DirectoryInfo resourcesDirectory = SafePath.GetDirectory(ProgramConstants.GetResourcePath());\n\n            if (!resourcesDirectory.Exists)\n                throw new DirectoryNotFoundException(\"Theme directory not found!\" + Environment.NewLine + ProgramConstants.RESOURCES_DIR);\n\n            Logger.Log(\"Initializing updater.\");\n\n            SafePath.DeleteFileIfExists(ProgramConstants.GamePath, \"version_u\");\n\n            Updater.Initialize(ProgramConstants.GamePath, ProgramConstants.GetBaseResourcePath(), ClientConfiguration.Instance.SettingsIniName, ClientConfiguration.Instance.LocalGame, SafePath.GetFile(ProgramConstants.StartupExecutable).Name);\n\n            Logger.Log(\"OSDescription: \" + RuntimeInformation.OSDescription);\n            Logger.Log(\"OSArchitecture: \" + RuntimeInformation.OSArchitecture);\n            Logger.Log(\"ProcessArchitecture: \" + RuntimeInformation.ProcessArchitecture);\n            Logger.Log(\"FrameworkDescription: \" + RuntimeInformation.FrameworkDescription);\n            Logger.Log(\"Selected OS profile: \" + MainClientConstants.OSId);\n            Logger.Log(\"Current culture: \" + CultureInfo.CurrentCulture);\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                // The query in CheckSystemSpecifications takes lots of time,\n                // so we'll do it in a separate thread to make startup faster\n                Thread thread = new Thread(CheckSystemSpecifications);\n                thread.Start();\n            }\n\n            // Using tasks here causes crashes on Wine for some reason\n            Thread onlineIdThread = new Thread(GenerateOnlineId);\n            onlineIdThread.Start();\n\n            if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares)\n                Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, \"debug\"), DateTime.Now.AddDays(-7)));\n\n            Task.Run(MigrateOldLogFiles);\n\n            // Start INI file preprocessor\n            PreprocessorBackgroundTask.Instance.Run();\n\n            DirectoryInfo updaterFolder = SafePath.GetDirectory(ProgramConstants.GamePath, \"Updater\");\n\n            if (updaterFolder.Exists)\n            {\n                Logger.Log(\"Attempting to delete temporary updater directory.\");\n                try\n                {\n                    updaterFolder.Delete(true);\n                }\n                catch\n                {\n                }\n            }\n\n            if (ClientConfiguration.Instance.CreateSavedGamesDirectory)\n            {\n                DirectoryInfo savedGamesFolder = SafePath.GetDirectory(ProgramConstants.GamePath, \"Saved Games\");\n\n                if (!savedGamesFolder.Exists)\n                {\n                    Logger.Log(\"Saved Games directory does not exist - attempting to create one.\");\n                    try\n                    {\n                        savedGamesFolder.Create();\n                    }\n                    catch\n                    {\n                    }\n                }\n            }\n\n            if (Updater.CustomComponents != null)\n            {\n                Logger.Log(\"Removing partial custom component downloads.\");\n                foreach (var component in Updater.CustomComponents)\n                {\n                    try\n                    {\n                        SafePath.DeleteFileIfExists(ProgramConstants.GamePath, FormattableString.Invariant($\"{component.LocalPath}_u\"));\n                    }\n                    catch\n                    {\n\n                    }\n                }\n            }\n\n            FinalSunSettings.WriteFinalSunIniAsync();\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                WriteInstallPathToRegistry();\n\n            ClientConfiguration.Instance.RefreshSettings();\n\n            using GameClass gameClass = new GameClass();\n\n            if (!UserINISettings.Instance.BorderlessWindowedClient)\n            {\n                // Find the largest recommended resolution as the default windowed resolution\n                var bestRecommendedResolution = ScreenResolution.GetBestRecommendedResolution();\n\n                UserINISettings.Instance.ClientResolutionX = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, \"ClientResolutionX\", bestRecommendedResolution.Width);\n                UserINISettings.Instance.ClientResolutionY = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, \"ClientResolutionY\", bestRecommendedResolution.Height);\n            }\n            else\n            {\n                // Find the largest fullscreen resolution as the default fullscreen resolution\n                var resolution = ScreenResolution.SafeFullScreenResolution;\n                UserINISettings.Instance.ClientResolutionX = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, \"ClientResolutionX\", resolution.Width);\n                UserINISettings.Instance.ClientResolutionY = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, \"ClientResolutionY\", resolution.Height);\n            }\n\n#if DEBUG\n            // Calculate hashes\n            {\n                FileHashCalculator fhc = new();\n                fhc.CalculateHashes();\n            }\n#endif\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n                Task.Run(InitSteamworks);\n\n            gameClass.Run();\n        }\n\n        [SupportedOSPlatform(\"windows\")]\n        private void InitSteamworks()\n        {\n            if (UserINISettings.Instance.SteamIntegration)\n            {\n                try\n                {\n                    if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares || ClientConfiguration.Instance.ClientGameType == ClientType.YR)\n                    {\n                        Logger.Log(\"Steam init called\");\n                        SteamClient.Init(2229850);\n                    }\n                    else if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)\n                    {\n                        Logger.Log(\"Steam init called\");\n                        SteamClient.Init(2229880);\n                    }\n                    else if (ClientConfiguration.Instance.ClientGameType == ClientType.RA)\n                    {\n                        Logger.Log(\"Steam init called\");\n                        SteamClient.Init(2229840);\n                    }\n                }\n                catch (System.Exception e)\n                {\n                    Logger.Log(\"Steam init failed: \" + e.Message);\n                    // Couldn't init for some reason (steam is closed etc)\n                }\n            }\n        }\n\n        /// <summary>\n        /// Recursively deletes all files from the specified directory that were created at <paramref name=\"pruneThresholdTime\"/> or before.\n        /// If directory is empty after deleting files, the directory itself will also be deleted.\n        /// </summary>\n        /// <param name=\"directory\">Directory to prune files from.</param>\n        /// <param name=\"pruneThresholdTime\">Time at or before which files must have been created for them to be pruned.</param>\n        private void PruneFiles(DirectoryInfo directory, DateTime pruneThresholdTime)\n        {\n            if (!directory.Exists)\n                return;\n\n            try\n            {\n                foreach (FileSystemInfo fsEntry in directory.EnumerateFileSystemInfos())\n                {\n                    if ((fsEntry.Attributes & FileAttributes.Directory) == FileAttributes.Directory)\n                        PruneFiles(new DirectoryInfo(fsEntry.FullName), pruneThresholdTime);\n                    else\n                    {\n                        try\n                        {\n                            FileInfo fileInfo = new FileInfo(fsEntry.FullName);\n                            if (fileInfo.CreationTime <= pruneThresholdTime)\n                                fileInfo.Delete();\n                        }\n                        catch (Exception ex)\n                        {\n                            Logger.Log(\"PruneFiles: Could not delete file \" + fsEntry.Name +\n                                \". Error message: \" + ex.ToString());\n                            continue;\n                        }\n                    }\n                }\n\n                if (!directory.EnumerateFileSystemInfos().Any())\n                    directory.Delete();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"PruneFiles: An error occurred while pruning files from \" +\n                   directory.Name + \". Message: \" + ex.ToString());\n            }\n        }\n\n        /// <summary>\n        /// Move log files from obsolete directories to currently used ones and adjust filenames to match currently used timestamp scheme.\n        /// </summary>\n        private void MigrateOldLogFiles()\n        {\n            MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, \"ClientCrashLogs\"), \"ClientCrashLog*.txt\");\n            MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, \"GameCrashLogs\"), \"EXCEPT*.txt\");\n            MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, \"SyncErrorLogs\"), \"SYNC*.txt\");\n        }\n\n        /// <summary>\n        /// Move log files matching given search pattern from specified directory to another one and adjust filename timestamps.\n        /// </summary>\n        /// <param name=\"newDirectory\">New log files directory.</param>\n        /// <param name=\"searchPattern\">Search string the log file names must match against to be copied. Can contain wildcard characters (* and ?) but doesn't support regular expressions.</param>\n        private static void MigrateLogFiles(DirectoryInfo newDirectory, string searchPattern)\n        {\n            DirectoryInfo currentDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, \"ErrorLogs\");\n            try\n            {\n                if (!currentDirectory.Exists)\n                    return;\n\n                if (!newDirectory.Exists)\n                    newDirectory.Create();\n\n                foreach (FileInfo file in currentDirectory.EnumerateFiles(searchPattern))\n                {\n                    string filenameTS = Path.GetFileNameWithoutExtension(file.Name);\n                    string[] ts = filenameTS.Split(new string[] { \"_\" }, StringSplitOptions.RemoveEmptyEntries);\n\n                    string timestamp = string.Empty;\n                    string baseFilename = Path.GetFileNameWithoutExtension(ts[0]);\n\n                    if (ts.Length >= 6)\n                    {\n                        timestamp = string.Format(\"_{0}_{1}_{2}_{3}_{4}\",\n                            ts[3], ts[2].PadLeft(2, '0'), ts[1].PadLeft(2, '0'), ts[4].PadLeft(2, '0'), ts[5].PadLeft(2, '0'));\n                    }\n\n                    string newFilename = SafePath.CombineFilePath(newDirectory.FullName, baseFilename, timestamp, file.Extension);\n                    file.MoveTo(newFilename);\n                }\n\n                if (!currentDirectory.EnumerateFiles().Any())\n                    currentDirectory.Delete();\n            }\n            catch (Exception ex)\n            {\n                Logger.Log(\"MigrateLogFiles: An error occured while moving log files from \" +\n                    currentDirectory.Name + \" to \" +\n                    newDirectory.Name + \". Message: \" + ex.ToString());\n            }\n        }\n\n        /// <summary>\n        /// Writes processor, graphics card and memory info to the log file.\n        /// </summary>\n        [SupportedOSPlatform(\"windows\")]\n        private static void CheckSystemSpecifications()\n        {\n            string cpu = string.Empty;\n            string videoController = string.Empty;\n            string memory = string.Empty;\n\n            ManagementObjectSearcher searcher;\n\n            try\n            {\n                searcher = new ManagementObjectSearcher(\"SELECT * FROM Win32_Processor\");\n\n                foreach (var proc in searcher.Get())\n                {\n                    cpu = cpu + proc[\"Name\"].ToString().Trim() + \" (\" + proc[\"NumberOfCores\"] + \" cores) \";\n                }\n\n            }\n            catch\n            {\n                cpu = \"CPU info not found\";\n            }\n\n            try\n            {\n                searcher = new ManagementObjectSearcher(\"SELECT * FROM Win32_VideoController\");\n\n                foreach (ManagementObject mo in searcher.Get())\n                {\n                    var currentBitsPerPixel = mo.Properties[\"CurrentBitsPerPixel\"];\n                    var description = mo.Properties[\"Description\"];\n                    if (currentBitsPerPixel != null && description != null)\n                    {\n                        if (currentBitsPerPixel.Value != null)\n                            videoController = videoController + \"Video controller: \" + description.Value.ToString().Trim() + \" \";\n                    }\n                }\n            }\n            catch\n            {\n                cpu = \"Video controller info not found\";\n            }\n\n            try\n            {\n                searcher = new ManagementObjectSearcher(\"Select * From Win32_PhysicalMemory\");\n                ulong total = 0;\n\n                foreach (ManagementObject ram in searcher.Get())\n                {\n                    total += Convert.ToUInt64(ram.GetPropertyValue(\"Capacity\"));\n                }\n\n                if (total != 0)\n                    memory = \"Total physical memory: \" + (total >= 1073741824 ? total / 1073741824 + \"GB\" : total / 1048576 + \"MB\");\n            }\n            catch\n            {\n                cpu = \"Memory info not found\";\n            }\n\n            Logger.Log(string.Format(\"Hardware info: {0} | {1} | {2}\", cpu.Trim(), videoController.Trim(), memory));\n        }\n\n        /// <summary>\n        /// Generate an ID for online play.\n        /// </summary>\n        private static void GenerateOnlineId()\n        {\n#if !WINFORMS\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n#endif\n#pragma warning disable format\n                try\n                {\n                    ManagementObjectCollection mbsList = null;\n                    ManagementObjectSearcher mbs = new ManagementObjectSearcher(\"Select * From Win32_processor\");\n                    mbsList = mbs.Get();\n                    string cpuid = \"\";\n\n                    foreach (ManagementObject mo in mbsList)\n                        cpuid = mo[\"ProcessorID\"].ToString();\n\n                    ManagementObjectSearcher mos = new ManagementObjectSearcher(\"SELECT * FROM Win32_BaseBoard\");\n                    var moc = mos.Get();\n                    string mbid = \"\";\n\n                    foreach (ManagementObject mo in moc)\n                        mbid = (string)mo[\"SerialNumber\"];\n\n                    string sid = new SecurityIdentifier((byte[])new DirectoryEntry(string.Format(\"WinNT://{0},Computer\", Environment.MachineName)).Children.Cast<DirectoryEntry>().First().InvokeGet(\"objectSID\"), 0).AccountDomainSid.Value;\n\n                    Connection.SetId(cpuid + mbid + sid);\n                    using RegistryKey key = Registry.CurrentUser.CreateSubKey(\"SOFTWARE\\\\\" + ClientConfiguration.Instance.InstallationPathRegKey);\n                    key.SetValue(\"Ident\", cpuid + mbid + sid);\n                }\n                catch (Exception)\n                {\n                    Random rn = new Random();\n\n                    using RegistryKey key = Registry.CurrentUser.CreateSubKey(\"SOFTWARE\\\\\" + ClientConfiguration.Instance.InstallationPathRegKey);\n                    string str = rn.Next(Int32.MaxValue - 1).ToString();\n\n                    try\n                    {\n                        Object o = key.GetValue(\"Ident\");\n                        if (o == null)\n                            key.SetValue(\"Ident\", str);\n                        else\n                            str = o.ToString();\n                    }\n                    catch { }\n\n                    Connection.SetId(str);\n                }\n#pragma warning restore format\n#if !WINFORMS\n            }\n            else\n            {\n                try\n                {\n                    string machineId = File.ReadAllText(\"/var/lib/dbus/machine-id\");\n\n                    Connection.SetId(machineId);\n                }\n                catch (Exception)\n                {\n                    Connection.SetId(new Random().Next(int.MaxValue - 1).ToString());\n                }\n            }\n#endif\n        }\n\n        /// <summary>\n        /// Writes the game installation path to the Windows registry.\n        /// </summary>\n        [SupportedOSPlatform(\"windows\")]\n        private static void WriteInstallPathToRegistry()\n        {\n            if (!UserINISettings.Instance.WritePathToRegistry)\n            {\n                Logger.Log(\"Skipping writing installation path to the Windows Registry because of INI setting.\");\n                return;\n            }\n\n            Logger.Log(\"Writing installation path to the Windows registry.\");\n\n            try\n            {\n                using RegistryKey key = Registry.CurrentUser.CreateSubKey(\"SOFTWARE\\\\\" + ClientConfiguration.Instance.InstallationPathRegKey);\n                key.SetValue(\"InstallPath\", ProgramConstants.GamePath);\n            }\n            catch\n            {\n                Logger.Log(\"Failed to write installation path to the Windows registry\");\n            }\n        }\n\n    }\n}"
  },
  {
    "path": "DXMainClient/app.PerMonitorV2.manifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <assemblyIdentity processorArchitecture=\"*\" version=\"1.0.0.0\" name=\"DXMainClient\" type=\"win32\"/>\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v2\">\n    <security>\n      <requestedPrivileges xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <!-- UAC Manifest Options\n             If you want to change the Windows User Account Control level replace the \n             requestedExecutionLevel node with one of the following.\n\n        <requestedExecutionLevel  level=\"asInvoker\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"requireAdministrator\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"highestAvailable\" uiAccess=\"false\" />\n\n            Specifying requestedExecutionLevel element will disable file and registry virtualization. \n            Remove this element if your application requires this virtualization for backwards\n            compatibility.\n        -->\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- A list of the Windows versions that this application has been tested on\n           and is designed to work with. Uncomment the appropriate elements\n           and Windows will automatically select the most compatible environment. -->\n\n      <!-- Windows Vista -->\n      <!-- <supportedOS Id=\"{e2011457-1546-43c5-a5fe-008deee3d3f0}\" /> -->\n\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />\n\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />\n\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />\n\n      <!-- Windows 10 & 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />\n\n    </application>\n  </compatibility>\n\t\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher\n       DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need \n       to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should \n       also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->\n      <!-- See https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/PM</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor</dpiAwareness>\n\n      <!-- Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->\n      <longPathAware xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">true</longPathAware>\n    </windowsSettings>\n  </application>\n\n  <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->\n  <dependency>\n    <dependentAssembly>\n      <assemblyIdentity\n          type=\"win32\"\n          name=\"Microsoft.Windows.Common-Controls\"\n          version=\"6.0.0.0\"\n          processorArchitecture=\"*\"\n          publicKeyToken=\"6595b64144ccf1df\"\n          language=\"*\"\n        />\n    </dependentAssembly>\n  </dependency>\n</assembly>\n"
  },
  {
    "path": "DXMainClient/app.SystemAware.manifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <assemblyIdentity processorArchitecture=\"*\" version=\"1.0.0.0\" name=\"DXMainClient\" type=\"win32\"/>\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v2\">\n    <security>\n      <requestedPrivileges xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <!-- UAC Manifest Options\n             If you want to change the Windows User Account Control level replace the \n             requestedExecutionLevel node with one of the following.\n\n        <requestedExecutionLevel  level=\"asInvoker\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"requireAdministrator\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"highestAvailable\" uiAccess=\"false\" />\n\n            Specifying requestedExecutionLevel element will disable file and registry virtualization. \n            Remove this element if your application requires this virtualization for backwards\n            compatibility.\n        -->\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- A list of the Windows versions that this application has been tested on\n           and is designed to work with. Uncomment the appropriate elements\n           and Windows will automatically select the most compatible environment. -->\n\n      <!-- Windows Vista -->\n      <!-- <supportedOS Id=\"{e2011457-1546-43c5-a5fe-008deee3d3f0}\" /> -->\n\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />\n\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />\n\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />\n\n      <!-- Windows 10 & 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />\n\n    </application>\n  </compatibility>\n\t\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher\n       DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need \n       to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should \n       also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->\n      <!-- See https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n\n      <!-- Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->\n      <longPathAware xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">true</longPathAware>\n    </windowsSettings>\n  </application>\n\n  <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->\n  <dependency>\n    <dependentAssembly>\n      <assemblyIdentity\n          type=\"win32\"\n          name=\"Microsoft.Windows.Common-Controls\"\n          version=\"6.0.0.0\"\n          processorArchitecture=\"*\"\n          publicKeyToken=\"6595b64144ccf1df\"\n          language=\"*\"\n        />\n    </dependentAssembly>\n  </dependency>\n</assembly>\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n  <PropertyGroup>\n    <LangVersion>14.0</LangVersion>\n    <ComVisible>false</ComVisible>\n    <CLSCompliant>false</CLSCompliant>\n    <ImplicitUsings>disable</ImplicitUsings>\n\n    <Title>CnCNet Client</Title>\n    <Company>CnCNet</Company>\n    <Product>CnCNet Client</Product>\n    <Copyright>Copyright © CnCNet, Rampastring 2011-2026</Copyright>\n    <Trademark>CnCNet</Trademark>\n    <!-- GitVersion will rewrite the informational version anyway -->\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\n    <!-- There is a race condition in GitVersion. Disable writing version info should help a bit -->\n    <WriteVersionInfoToBuildLog>false</WriteVersionInfoToBuildLog>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <Configurations>\n      UniversalGLDebug;WindowsDXDebug;WindowsGLDebug;WindowsXNADebug;\n      UniversalGLRelease;WindowsDXRelease;WindowsGLRelease;WindowsXNARelease\n    </Configurations>\n  </PropertyGroup>\n\n  <!-- For Internal Logic -->\n  <PropertyGroup>\n    <!-- Rendering Engine -->\n    <Engine Condition=\"$(Configuration.Contains(WindowsDX))\">WindowsDX</Engine>\n    <Engine Condition=\"$(Configuration.Contains(UniversalGL))\">UniversalGL</Engine>\n    <Engine Condition=\"$(Configuration.Contains(WindowsGL))\">WindowsGL</Engine>\n    <Engine Condition=\"$(Configuration.Contains(WindowsXNA))\">WindowsXNA</Engine>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(MSBuildProjectName)' == 'ClientGUI' Or '$(MSBuildProjectName)' == 'DXMainClient'\">\n    <TargetFrameworks Condition=\"$(Engine.Contains(Windows))\">net48;net8.0-windows</TargetFrameworks>\n    <TargetFrameworks Condition=\"!$(Engine.Contains(Windows))\">net8.0</TargetFrameworks>\n\n    <!-- We need to define both platforms to make Visual Studio happy when targeting XNA -->\n    <Platforms>AnyCPU;x86</Platforms>\n    <!-- XNA only supports x86, but the other engines support any CPU. -->\n    <Platform Condition=\"$(Engine.Contains(XNA))\">x86</Platform>\n    <Platform Condition=\"!$(Engine.Contains(XNA))\">AnyCPU</Platform>\n    <PlatformTarget Condition=\"$(Engine.Contains(XNA))\">x86</PlatformTarget>\n    <PlatformTarget Condition=\"!$(Engine.Contains(XNA))\">AnyCPU</PlatformTarget>\n    <!-- WinForms Auto Configure -->\n    <UseWindowsForms Condition=\"$(Engine.Contains(Windows))\">true</UseWindowsForms>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(MSBuildProjectName)' == 'TranslationNotifierGenerator'\">\n    <TargetFrameworks>netstandard2.0</TargetFrameworks>\n    <Platforms>AnyCPU</Platforms>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(MSBuildProjectName)' == 'ClientCore' Or '$(MSBuildProjectName)' == 'SecondStageUpdater' Or '$(MSBuildProjectName)' == 'ClientUpdater'\">\n    <TargetFrameworks>net8.0;net48</TargetFrameworks>\n    <Platforms>AnyCPU</Platforms>\n  </PropertyGroup>\n\n  <!-- For Constants -->\n  <PropertyGroup>\n    <!-- Is Debug -->\n    <DefineConstants Condition=\"$(Configuration.Contains(Debug))\">$(DefineConstants);DEBUG</DefineConstants>\n\n    <!-- Engines -->\n    <DefineConstants Condition=\"$(Engine.Contains('DX'))\">$(DefineConstants);DX</DefineConstants>\n    <DefineConstants Condition=\"$(Engine.Contains('GL'))\">$(DefineConstants);GL</DefineConstants>\n    <DefineConstants Condition=\"$(Engine.Contains('XNA'))\">$(DefineConstants);XNA</DefineConstants>\n    <DefineConstants Condition=\"$(Engine.Contains('Windows'))\">$(DefineConstants);ISWINDOWS</DefineConstants>\n    <DefineConstants Condition=\"'$(UseWindowsForms)' == 'true'\">$(DefineConstants);WINFORMS</DefineConstants>\n  </PropertyGroup>\n\n  <!-- Output Path Hack -->\n  <PropertyGroup>\n    <ClientConfiguration Condition=\"$(Configuration.Contains(Debug))\">Debug</ClientConfiguration>\n    <ClientConfiguration Condition=\"$(Configuration.Contains(Release))\">Release</ClientConfiguration>\n    <OutputPathSuffix>$(ClientConfiguration)\\$(Engine)\\</OutputPathSuffix>\n    <OutputPath Condition=\"'$(ClientConfiguration)' != ''\">$(BaseOutputPath)bin\\$(OutputPathSuffix)</OutputPath>\n    <IntermediateOutputPath Condition=\"'$(ClientConfiguration)' != ''\">$(BaseIntermediateOutputPath)obj\\$(OutputPathSuffix)</IntermediateOutputPath>\n    <ArtifactsPivots>$(OutputPathSuffix)$(TargetFramework)</ArtifactsPivots>\n  </PropertyGroup>\n\n  <!-- Support WindowsXNA 32bit debugging in VS -->\n  <PropertyGroup Condition=\"'$(PlatformTarget)' == 'x86' And '$(TargetFrameworkIdentifier)' != '.NETFramework'\">\n    <RunCommand Condition=\"Exists('$(MSBuildProgramFiles32)\\dotnet\\dotnet.exe')\">$(MSBuildProgramFiles32)\\dotnet\\dotnet.exe</RunCommand>\n  </PropertyGroup>\n\n  <!-- Allow a game specific build prop file to be imported, if available -->\n  <Import Project=\"$(MSBuildThisFileDirectory)Directory.props\" Condition=\"Exists('$(MSBuildThisFileDirectory)Directory.props')\" />\n\n  <ItemGroup>\n    <CompilerVisibleProperty Include=\"RootNamespace\" />\n  </ItemGroup>\n\n  <PropertyGroup>\n    <XNAUIRoot>$(MSBuildThisFileDirectory)Rampastring.XNAUI</XNAUIRoot>\n  </PropertyGroup>\n\n  <ItemGroup Condition=\"('$(MSBuildProjectName)' == 'ClientGUI' Or '$(MSBuildProjectName)' == 'DXMainClient') And $(Engine.Contains(XNA))\">\n    <Reference Include=\"Microsoft.Xna.Framework\">\n      <HintPath>$(XNAUIRoot)\\References\\XNA\\Microsoft.Xna.Framework.dll</HintPath>\n    </Reference>\n    <Reference Include=\"Microsoft.Xna.Framework.Game\">\n      <HintPath>$(XNAUIRoot)\\References\\XNA\\Microsoft.Xna.Framework.Game.dll</HintPath>\n    </Reference>\n    <Reference Include=\"Microsoft.Xna.Framework.Graphics\">\n      <HintPath>$(XNAUIRoot)\\References\\XNA\\Microsoft.Xna.Framework.Graphics.dll</HintPath>\n    </Reference>\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "Directory.Build.targets",
    "content": "<Project>\n  <Target Name=\"ShowBuildInfo\" BeforeTargets=\"CoreCompile\" Condition=\"'$(MSBuildProjectName)' == 'DXMainClient'\">\n    <Message Importance=\"high\" Text=\"Engine: $(Engine); Platform: $(Platform); TargetFramework: $(TargetFramework); Configuration: $(Configuration)\" />\n  </Target>\n\n  <!-- \"GetVersion\" target is defined in GitVersion package -->\n  <Target Name=\"NonReleaseBuildWarning\" AfterTargets=\"GetVersion\" BeforeTargets=\"CoreCompile\" Condition=\"'$(MSBuildProjectName)' == 'DXMainClient' AND ($(GitVersion_CommitsSinceVersionSource) != 0)\">\n    <PropertyGroup>\n      <DefineConstants>$(DefineConstants);DEVELOPMENT_BUILD</DefineConstants>\n    </PropertyGroup>\n    <Warning Text=\"This is a development build of the client. Stability and reliability may not be fully guaranteed.\" Condition=\"'$(BuildingInsideVisualStudio)' != 'true'\"></Warning>\n  </Target>\n\n  <Target Name=\"RestoreUpdater\" AfterTargets=\"Restore\" Condition=\"'$(PublishDir)' != '' AND '$(MSBuildProjectName)' == 'DXMainClient'\">\n    <MSBuild\n      Projects=\"$(MSBuildThisFileDirectory)SecondStageUpdater\\SecondStageUpdater.csproj\"\n      Properties=\"TargetFramework=$(TargetFramework.Split('-')[0]);Platform=AnyCPU;RuntimeIdentifier=\"\n      Targets=\"Restore\" />\n  </Target>\n\n  <Target Name=\"BuildUpdater\" AfterTargets=\"Build\" Condition=\"'$(PublishDir)' != '' AND '$(MSBuildProjectName)' == 'DXMainClient'\">\n    <MSBuild\n      Projects=\"$(MSBuildThisFileDirectory)SecondStageUpdater\\SecondStageUpdater.csproj\"\n      Properties=\"TargetFramework=$(TargetFramework.Split('-')[0]);Platform=AnyCPU;RuntimeIdentifier=\" />\n  </Target>\n\n  <Target Name=\"MakeDirectoryStructure\" AfterTargets=\"Publish\" Condition=\"'$(MSBuildProjectName)' == 'DXMainClient'\">\n    <CallTarget Targets=\"PublishNetFrameworkWindowsGLNative;RemoveNetFrameworkWindowsGLNative;RemoveWindowsDXNonWindowsBinaries;RemoveWindowsGLNonWindowsBinaries;RemoveGLMobileBinaries\" />\n    <CallTarget Targets=\"MoveCommonBinaries;MoveClientExes;MoveUpdater\" Condition=\"'$(NoMove)' != 'true'\" />\n  </Target>\n\n  <Target Name=\"PublishNetFrameworkWindowsGLNative\" Condition=\"'$(TargetFrameworkIdentifier)' == '.NETFramework' And '$(Engine)' == 'WindowsGL'\">\n    <Message Importance=\"high\" Text=\"Copying NetFramework WindowsGLNative files\" />\n    <ItemGroup>\n      <_lib_x64 Include=\"$(OutputPath)\\x64\\*.*\" />\n      <_lib_x86 Include=\"$(OutputPath)\\x86\\*.*\" />\n    </ItemGroup>\n    <Copy SourceFiles=\"@(_lib_x64)\" DestinationFolder=\"$(PublishDir)\\x64\" />\n    <Copy SourceFiles=\"@(_lib_x86)\" DestinationFolder=\"$(PublishDir)\\x86\" />\n  </Target>\n\n  <Target Name=\"RemoveNetFrameworkWindowsGLNative\" AfterTargets=\"PublishNetFrameworkWindowsGLNative\">\n    <Message Importance=\"high\" Text=\"Removing unnecessary NetFramework WindowsGLNative files\" />\n    <Delete Files=\"$(PublishDir)SDL2.dll\" />\n    <Delete Files=\"$(PublishDir)soft_oal.dll\" />\n  </Target>\n\n  <Target Name=\"RemoveWindowsDXNonWindowsBinaries\" Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows' And '$(Engine)' == 'WindowsDX'\">\n    <Message Importance=\"high\" Text=\"Removing unnecessary WindowsDX files\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/debian-x64\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/fedora-x64\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/opensuse-x64\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/osx\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/rhel-x64\" />\n  </Target>\n\n  <Target Name=\"RemoveWindowsGLNonWindowsBinaries\" Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows' And '$(Engine)' == 'WindowsGL'\">\n    <Message Importance=\"high\" Text=\"Removing unnecessary WindowsGL files\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/linux-x64\" />\n    <RemoveDir Directories=\"$(PublishDir)runtimes/osx\" />\n  </Target>\n\n  <Target Name=\"RemoveGLMobileBinaries\" Condition=\"'$(TargetFrameworkIdentifier)' != '.NETFramework' And ('$(Engine)' == 'UniversalGL' Or '$(Engine)' == 'WindowsGL')\">\n    <Message Importance=\"high\" Text=\"Removing unnecessary GL files\" />\n\n    <!-- Note: You might need to update folders here if the dependency `MonoGame.Framework.DesktopGL` (specifically, its dependency `MonoGame.Library.OpenAL`) gets updated. -->\n    <ItemGroup>\n      <!-- https://stackoverflow.com/a/71699878 -->\n      <_GLAndroidDirectories Include=\"$([System.IO.Directory]::GetDirectories($(PublishDir)runtimes/, 'android*', System.IO.SearchOption.TopDirectoryOnly))\" />\n      <_GLIosDirectories Include=\"$([System.IO.Directory]::GetDirectories($(PublishDir)runtimes/, 'ios*', System.IO.SearchOption.TopDirectoryOnly))\" />\n    </ItemGroup>\n\n    <RemoveDir Directories=\"@(_GLAndroidDirectories)\" Condition=\"'@(_GLAndroidDirectories)' != ''\" />    \n    <RemoveDir Directories=\"@(_GLIosDirectories)\" Condition=\"'@(_GLIosDirectories)' != ''\" />\n  </Target>\n\n  <!-- IMPORTANT: You SHOULD NOT modify the logic here if you don't have to. -->\n  <!-- The list of common assemblies has been moved to the `CommonAssemblies.txt` and `CommonAssembliesNetFx.txt` files. -->\n  <!-- And you SHOULD modify these two files following Scripts/README.md file! -->\n  <Target Name=\"MoveCommonBinaries\">\n    <PropertyGroup>\n      <_CommonAssembliesFilePath Condition=\"'$(TargetFrameworkIdentifier)' != '.NETFramework'\">$(MSBuildThisFileDirectory)CommonAssemblies.txt</_CommonAssembliesFilePath>\n      <_CommonAssembliesFilePath Condition=\"'$(TargetFrameworkIdentifier)' == '.NETFramework'\">$(MSBuildThisFileDirectory)CommonAssembliesNetFx.txt</_CommonAssembliesFilePath>\n    </PropertyGroup>\n    <Message Importance=\"high\" Text=\"Moving common binaries\" />\n    <ReadLinesFromFile File=\"$(_CommonAssembliesFilePath)\">\n      <Output TaskParameter=\"Lines\" ItemName=\"_CommonFiles\" />\n    </ReadLinesFromFile>\n    <Move SourceFiles=\"$(PublishDir)%(_CommonFiles.Identity)\" DestinationFolder=\"$(PublishDir)..\\\" Condition=\"!Exists('$(PublishDir)..\\%(_CommonFiles.Identity)')\" />\n    <Delete Files=\"$(PublishDir)%(_CommonFiles.Identity)\" Condition=\"Exists('$(PublishDir)..\\%(_CommonFiles.Identity)')\" />\n  </Target>\n\n  <!-- Note: the folder structure should be consistent with scripts\\build.ps1 file -->\n  <Target Name=\"MoveClientExes\" Condition=\"'$(TargetFrameworkIdentifier)' == '.NETFramework'\">\n    <Message Importance=\"high\" Text=\"Moving client executables\" />\n    <Move SourceFiles=\"$(PublishDir)clientdx.exe\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientdx.exe')\" />\n    <Move SourceFiles=\"$(PublishDir)clientdx.exe.config\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientdx.exe.config')\" />\n    <Move SourceFiles=\"$(PublishDir)clientdx.pdb\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientdx.pdb')\" />\n    <Move SourceFiles=\"$(PublishDir)clientogl.exe\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientogl.exe')\" />\n    <Move SourceFiles=\"$(PublishDir)clientogl.exe.config\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientogl.exe.config')\" />\n    <Move SourceFiles=\"$(PublishDir)clientogl.pdb\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientogl.pdb')\" />\n    <Move SourceFiles=\"$(PublishDir)clientxna.exe\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientxna.exe')\" />\n    <Move SourceFiles=\"$(PublishDir)clientxna.exe.config\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientxna.exe.config')\" />\n    <Move SourceFiles=\"$(PublishDir)clientxna.pdb\" DestinationFolder=\"$(PublishDir)../../\" Condition=\"Exists('$(PublishDir)clientxna.pdb')\" />\n  </Target>\n\n  <!-- Note: only the debug build should be started with the debugger, release builds will fail with a 'Could not find Resource directory' error -->\n  <Target Name=\"CopyResources\" AfterTargets=\"Build\" Condition=\"$(DefineConstants.Contains('DEBUG'))\">\n    <ItemGroup>\n      <ExampleClientResources Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\DTA\\**\\*.*\" />\n      <ExampleClientMaps Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\Maps\\**\\*.*\" />\n      <ExampleClientIni Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\INI\\**\\*.*\" />\n      <ExampleClientMix Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\MIX\\**\\*.*\" />\n      <ExampleClientSettings Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\SUN.ini\" />\n      <ExampleClientDefinitions Include=\"$(MSBuildThisFileDirectory)\\DXMainClient\\Resources\\ClientDefinitions.ini\" />\n    </ItemGroup>\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientResources)\" DestinationFolder=\"$(OutputPath)\\Resources\\%(RecursiveDir)\" />\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientMaps)\" DestinationFolder=\"$(OutputPath)\\Maps\\%(RecursiveDir)\" />\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientIni)\" DestinationFolder=\"$(OutputPath)\\INI\\%(RecursiveDir)\" />\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientMix)\" DestinationFolder=\"$(OutputPath)\\MIX\\%(RecursiveDir)\" />\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientSettings)\" DestinationFolder=\"$(OutputPath)\" />\n    <Copy Condition=\"! Exists('$(OutputPath)\\Resources\\ClientDefinitions.ini') \" SourceFiles=\"@(ExampleClientDefinitions)\" DestinationFolder=\"$(OutputPath)\\Resources\" />\n  </Target>\n\n  <Target Name=\"CopyUpdater\" AfterTargets=\"Build\" Condition=\"'$(PublishDir)' != '' AND '$(MSBuildProjectName)' == 'SecondStageUpdater'\">\n    <PropertyGroup>\n      <CNCNetUpdaterCopyTo>$(PublishDir)\\..\\Updater\\</CNCNetUpdaterCopyTo>\n    </PropertyGroup>\n    <ItemGroup>\n      <CNCNetUpdaterOutputFile Include=\"$(OutputPath)\\*.*\" />\n    </ItemGroup>\n    <Copy SourceFiles=\"@(CNCNetUpdaterOutputFile)\" DestinationFolder=\"$(CNCNetUpdaterCopyTo)\" />\n  </Target>\n\n  <Target Name=\"MoveUpdater\" Condition=\"'$(MSBuildProjectName)' == 'SecondStageUpdater'\">\n    <Message Importance=\"high\" Text=\"Moving updater executables\" />\n    <Move SourceFiles=\"%(CNCNetUpdaterOutputFile.Identity)\" DestinationFolder=\"$(CNCNetUpdaterCopyTo)\" Condition=\"!Exists('$(CNCNetUpdaterCopyTo)%(CNCNetUpdaterOutputFile.Identity)')\" />\n    <Delete Files=\"%(CNCNetUpdaterOutputFile.Identity)\" Condition=\"Exists('$(CNCNetUpdaterCopyTo)%(CNCNetUpdaterOutputFile.Identity)')\" />\n  </Target>\n\n  <!-- Allow a game specific build prop file to be imported, if available -->\n  <Import Project=\"$(MSBuildThisFileDirectory)Directory.targets\" Condition=\"Exists('$(MSBuildThisFileDirectory)Directory.targets')\" />\n\n</Project>\n"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n    <DotnetLibrariesVersion>8.0.0</DotnetLibrariesVersion>\n  </PropertyGroup>\n  <ItemGroup>\n    <GlobalPackageReference Include=\"GitVersion.MsBuild\" Version=\"5.12.0\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageVersion Include=\"DiscordRichPresence\" Version=\"1.2.1.24\" />\n    <PackageVersion Include=\"Facepunch.Steamworks\" Version=\"2.4.1\" />\n    <PackageVersion Include=\"ImeSharp\" Version=\"1.4.1\" />\n    <PackageVersion Include=\"lzo.net\" Version=\"0.0.6\" />\n    <PackageVersion Include=\"Microsoft.AspNet.WebApi.Client\" Version=\"6.0.0\" />\n    <PackageVersion Include=\"Microsoft.CodeAnalysis.Analyzers\" Version=\"3.3.3\" />\n    <PackageVersion Include=\"Microsoft.CodeAnalysis.CSharp.Workspaces\" Version=\"4.4.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Hosting\" Version=\"$(DotnetLibrariesVersion)\" />\n    <PackageVersion Include=\"OpenMcdf\" Version=\"2.4.1\" />\n    <PackageVersion Include=\"SixLabors.ImageSharp\" Version=\"2.1.11\" />\n    <PackageVersion Include=\"System.DirectoryServices\" Version=\"$(DotnetLibrariesVersion)\" />\n    <PackageVersion Include=\"System.Management\" Version=\"$(DotnetLibrariesVersion)\" />\n    <PackageVersion Include=\"System.Private.Uri\" Version=\"4.3.2\" />\n    <PackageVersion Include=\"System.Text.Encoding.CodePages\" Version=\"$(DotnetLibrariesVersion)\" />\n    <PackageVersion Include=\"System.Text.Json\" Version=\"8.0.6\" />\n    <PackageVersion Include=\"Ude.NetStandard\" Version=\"1.2.0\" />\n  </ItemGroup>\n  <ItemGroup Condition=\"'$(MSBuildProjectName)' == 'ClientCore' Or '$(MSBuildProjectName)' == 'ClientGUI' Or '$(MSBuildProjectName)' == 'DTAConfig' Or '$(MSBuildProjectName)' == 'DXMainClient' Or '$(MSBuildProjectName)' == 'ClientUpdater'\">\n    <ProjectReference Include=\"$(MSBuildThisFileDirectory)TranslationNotifierGenerator\\TranslationNotifierGenerator.csproj\" OutputItemType=\"Analyzer\" ReferenceOutputAssembly=\"false\" />\n  </ItemGroup>\n  <!-- Polyfill on .NET 4.8 (SecondStageUpdater excluded) -->\n  <ItemGroup Condition=\"'$(MSBuildProjectName)' != 'SecondStageUpdater' And $(TargetFrameworkIdentifier) == '.NETFramework'\">\n    <GlobalPackageReference Include=\"Polyfill\" Version=\"9.9.0\" />\n    <PackageReference Include=\"System.ValueTuple\" Condition=\"$(TargetFramework.StartsWith('net46'))\" />\n    <PackageVersion Include=\"System.ValueTuple\" Version=\"4.5.0\" />\n    <PackageReference Include=\"System.Memory\" Condition=\"$(TargetFrameworkIdentifier) == '.NETStandard' or $(TargetFrameworkIdentifier) == '.NETFramework' or $(TargetFramework.StartsWith('netcoreapp2'))\" />\n    <PackageVersion Include=\"System.Memory\" Version=\"4.6.3\" />\n    <PackageReference Include=\"System.Threading.Tasks.Extensions\" Condition=\"$(TargetFramework) == 'netstandard2.0' or $(TargetFramework) == 'netcoreapp2.0' or $(TargetFrameworkIdentifier) == '.NETFramework'\" />\n    <PackageVersion Include=\"System.Threading.Tasks.Extensions\" Version=\"4.6.3\" />\n    <PackageReference Include=\"System.Runtime.InteropServices.RuntimeInformation\" Condition=\"$(TargetFrameworkIdentifier) == '.NETFramework'\" />\n    <PackageVersion Include=\"System.Runtime.InteropServices.RuntimeInformation\" Version=\"4.3.0\" />\n    <Reference Include=\"System.IO.Compression\" />\n  </ItemGroup>\n  <!-- I don't know how to write it. but I think we should make a note at here. -->\n  <ItemGroup Condition=\"'$(MSBuildProjectName)' == 'DXMainClient'\">\n    <!-- These two packages are explicitly imported to get rid of Error NU1605 Detected package downgrade. -->\n    <!-- This error is only raised when both .NET 4 and .NET 8 exists in TargetFrameworks: -->\n    <!-- <TargetFrameworks>net48;net8.0-windows</TargetFrameworks> -->\n    <!-- and -p:Engine=WindowsDX -f net48 -->\n    <PackageReference Include=\"NETStandard.Library\" />\n    <PackageVersion Include=\"NETStandard.Library\" Version=\"2.0.3\" />\n    <PackageReference Include=\"System.IO.FileSystem\" />\n    <PackageVersion Include=\"System.IO.FileSystem\" Version=\"4.3.0\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "Docs/Build.md",
    "content": "# Build & Publish #\n\nThe information below describes the steps that the default build script (build.ps1) performs.\n\nEngine configurations\n---------------------\n\nThe client uses different APIs to render itself, called Engine. Engine defines the technology used and the platform where the client can be launched:\n\nAny platform (Windows, Linux, macOS):\n* UniversalGL\n\nOnly Windows:\n* WindowsDX\n* WindowsGL\n* WindowsXNA\n\nTargetFramework configurations\n------------------------------\n\nFor each Engine configuration one or more TargetFrameworks will be build:\n\nUniversalGL:\n* net8.0\n\nWindowsDX, WindowsGL & WindowsXNA:\n* net4.8\n\nOverview of the Engine configurations differences:\n\n| Configuration | OS Support | Default Platform | Technology                    |\n| ------------- | ---------- | ---------------- | ----------------------------- |\n| UniversalGL   | Any        | AnyCPU           | MonoGame DesktopGL            |\n| WindowsDX     | Windows    | AnyCPU           | MonoGame WindowsDX + WinForms |\n| WindowsGL     | Windows    | AnyCPU           | MonoGame DesktopGL + WinForms |\n| WindowsXNA    | Windows    | x86              | Microsoft XNA + WinForms      |\n\nBuild output\n------------\n\nThe build output when using the `dotnet publish` command is created in `\\Compiled`.\n\nLaunching the client is done by running e.g.: `dotnet clientogl.dll`\n\nBuilding without IDE\n--------------------\n\nTo 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.\n\nBuilding with Visual Studio\n---------------------------\n\n> [!IMPORTANT] \n> IDEs can build Release configurations, but they are forbidden to run due to compile-time optimizations on binaries.\n\nYou can select the desired configuration directly from the solution configurations:\n\n![VSBUILDCONF](Images/vs-build-configuration.png)\n\nNote that the XNA configurations can only be built/debugged with `x86`.\n\n![VSBUILDCPU](Images/vs-cpu-configuration.png)\n\n> [!WARNING]\n> 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.\n\nBuilding with Rider\n-------------------\n\nSelect desired configuration from list in the top panel:\n\n![RIDERCONF](Images/rider-configuration-dropdown.png)\n\n"
  },
  {
    "path": "Docs/DiscordRichPresence.md",
    "content": "# Discord Rich Presence support in XNA CnCNet client\n\nAbout Discord Rich Presence\n-----------------------------------\nDiscord 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.\n\n![DEMO](Images/drp-demo.gif)\n\nThe 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.\n\nXNA 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.\n\nHow to set up DRP for your mod/game\n-----------------------------------\n\n> [!NOTE]\n> You are required to be logged in a Discord account.\n\n1. Go to [Discord developers portal](https://discord.com/developers/applications).\n2. 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.\n3. 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.\n![ID](Images/drp-id.png)\n4. 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:\n   - 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);\n   - 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:\n     - `Nod Genesis Legion` -> `nodgenesislegion`,\n     - `Yuri's Legi0n` -> `yurislegi0n`,\n     - `Random Allies` -> `randomallies`,\n     - `Spectator` -> `spectator` etc.\n\n   After you upload the images, click the `Save Changes` button.\n![ASSETS](Images/drp-assets.png)\n\n> [!NOTE]\n> 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.\n"
  },
  {
    "path": "Docs/HowToUpdate.md",
    "content": "# How to update your mod to latest client version\n\nThis 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.).\n\n> [!WARNING]\n> 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!\n\n## Updating the XNA CnCNet Client binaries for the package\n\n1. **Download the latest client binaries release:**\n   - Find the latest released client from [XNA CnCNet Client repo releases page](https://github.com/CnCNet/xna-cncnet-client/releases).\n   - Download the `[xna-cncnet-client-X.Y.Z.7z]` file, inside of which the updated `Resources` folder should reside.\n   - Make note of any migration steps noted in the release.\n\n2. **Clean up old binaries folders:**\n   - Go to your local game/mod repo or working folder.\n   - Find `Resources/` folder inside of the \"game root\" folder.\n   - **IMPORTANT:** Delete `Binaries` and `BinariesNET8` to ensure that no obsolete/renamed library files remain.\n\n3. **Paste files into the package repository:**\n   - Go to your local game/mod repo or working folder.\n   - Unarchive `Resources` folder from `[xna-cncnet-client-X.Y.Z.7z]` file downloaded earlier inside the \"game root\" folder.\n   - You **must** get a prompt to replace `Resources/` folder and files inside it. If not, you're in the wrong directory.\n\n4. **Apply the migration steps:**\n   - If updating to next version: follow the instructions from release notes mentioned in step 1.\n   - If updating skipping multiple versions, either:\n     - look up all release notes skipped and apply migrations;\n     - or refer to the [client docs on migration](Migration.md).\n\n5. **Run the packaged client to test:**\n   - Launch: `YourClientLauncher.exe` on Windows or `YourClientLauncher.sh` on Linux/Mac (the names will vary depending on the mod/game client package).\n   - Verify the version hash and client version in the online lobby.\n   - Does it work? If no - you missed some migration steps or screwed up somewhere in the steps above, verify your changes or start anew.\n\nAfter that you can commit/push the changes, if using Git, or publish an update\n"
  },
  {
    "path": "Docs/INISystem.md",
    "content": "﻿# Instructions on how to construct the UI using INI files.\n> [!NOTE]\n> _TODO work in progress_\n\n## Constants\nThe `[ParserConstants]` section of the `GlobalThemeSettings.ini` file contains constants that can be used in other INI files.\n\n### Predefined System Constants\n`RESOLUTION_WIDTH`: the width of the window when it is initialized  \n`RESOLUTION_HEIGHT`: the height of the window when it is initialized  \n\n### User Defined Constants\n\n```ini\nMY_EXAMPLE_CONSTANT=15\n```\n\nThe above user-defined or system constants can be used elsewhere as:\n\n```ini\n[MyExampleControl]\n$X=MY_EXAMPLE_CONSTANT\n```\n_NOTE: Constants can only be used in [dynamic control properties](#dynamic-control-properties)_\n\n### Data Types\n\n- 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.\n- 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`.\n- 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`. \n- The `integer` type is actually `System.Int32`.\n- The `float` type is actually `System.Single`.\n- 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.\n- The `comma-separated strings` is a string, but separated with `,` character without spaces e.g., `one,two,three`.\n<!-- - The `comma separated integers` or `comma separated floats` is a `integer` or `float` type, but separated with `,` character without spaces e.g., `0,0` or `0.0,0.0` respectively. -->\n\n## Control Properties\n\nBelow 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.\n\n### Basic Control Properties\n\nBasic control properties cannot use constants.\n> [!WARNING]\n> Do not copy-paste ini-code below without edits because it won't work! It shows only how to work with properties.\n> \n> For example,\n> - `X` and `Y` are conflict with `Location`,\n> - `BackgroundTexture` and `SolidColorBackgroundTexture` conflicts,\n> - and many others.\n\n#### [XNAControl](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAControl.cs)\n\n- Basic class inherited by any other control element.\n\n```ini\n[SOMECONTROL]                      ; XNAControl\nX=                                 ; integer,    the X location of the control.\nY=                                 ; integer,    the Y location of the control.\nLocation=                          ; 2 integers, the X and Y location of the control.\nWidth=                             ; integer,    the Width of the control.\nHeight=                            ; integer,    the Height of the control.\nSize=                              ; 2 integers, the Width and Height of the control.\nText=                              ; text,       the text to display for the control (ex: buttons, labels, etc...).\nVisible=true                       ; boolean,    whether or not the control should be visible by default.\nEnabled=true                       ; boolean,    whether or not the control can be interacted with by default.\nDistanceFromRightBorder=0          ; integer,    the distance of the right edge of this control from \n                                   ;             the right edge of its parent. This control MUST have a parent.\nDistanceFromBottomBorder=0         ; integer,    the distance of the bottom edge of this control from the \n                                   ;             bottom edge of its parent. This control MUST have a parent.\nFillWidth=0                        ; integer,    this will set the width of this control to fill \n                                   ;             the parent/window MINUS this value, starting from the its X position.\nFillHeight=0                       ; integer,    this will set the height of this control to fill \n                                   ;             the parent/window MINUS this value, starting from the its Y position.\nDrawOrder=0                        ; integer,    determine the layering order of the control within \n                                   ;             its parent control's list of child controls.\nUpdateOrder=0                      ; integer,    determine the layering order of the control within \n                                   ;             its parent control's list of child controls.\nRemapColor=255,255,255             ; color,      this will set a theme defined color based.\nControlDrawMode=UniqueRenderTarget ; enum (UniqueRenderTarget | Normal), \n                                   ;             this will set render option to draw control on its own render \n                                   ;             target (`UniqueRenderTarget`) or to draw control on \n                                   ;             the same render target with its parent (`Normal`).\n```\n\n#### [XNAIndicator](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAIndicator.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMEINDICATOR]            ; XNAIndicator\nFontIndex=0                ; integer, the index of font loaded from font list. Default value is `0`.\nHighlightColor=255,255,255 ; color,   the text color when cursor above the `XNAIndicator`.\nAlphaRate=0.1              ; float,   the indicator's transparency changing rate per 100 milliseconds. \n                           ;          If the indicator is transparent, it'll become non-transparent at this rate. \n```\n\n#### [XNAPanel](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAPanel.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMEPANEL]                  ; XNAPanel\nBorderColor=196,196,196      ; color,      this will set a border color based.\nAlphaRate=0.01               ; float,      the panel's transparency changing rate per 100 milliseconds.\n                             ;             If the panel is transparent, it'll become non-transparent at this rate.\nBackgroundTexture=           ; string,     loads a texture with the specific file name with suffix.\n                             ;             If the texture isn't found from any asset search path,\n                             ;             returns a dummy texture.\nSolidColorBackgroundTexture= ; color,      this will set background color stretched texture instead of \n                             ;             user defined picture.\nDrawBorders=true             ; boolean,    enables or disables borders drawing for control. \n                             ;             Borders enabled by default.\nPadding=                     ; 4 integers, css-like panel padding in client window e.g.,\n                             ;             `1,2,3,4` where `1` - left, `2` - top, `3` - right, `4` - bottom.\nDrawMode=Stretched           ; enum (Tiled | Centered | Stretched), \n                             ;             this will set draw mode for panel.\n```\n\n#### [XNAExtraPanel](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAExtraPanel.cs)\n\n_(inherits [XNAPanel](#XNAPanel))_\n\n```ini\n[SOMEEXTRAPANEL]   ; XNAExtraPanel\nBackgroundTexture= ; string, same as XNAControl's `BackgroundTexture`.\n```\n\n#### [XNATextBlock](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATextBlock.cs)\n\n_(inherits [XNAPanel](#XNAPanel))_\n\n```ini\n[SOMETEXTBLOCK]       ; XNATextBlock\nTextColor=196,196,196 ; color, defines text color for text block.\n```\n\n#### [XNAMultiColumnListBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAMultiColumnListBox.cs)\n\n_(inherits [XNAPanel](#XNAPanel))_\n\n```ini\n[SOMEMULTICOLUMBLISTBOX]         ; XNAMultiColumnListBox\nFontIndex=0                      ; integer,        the index of font loaded from font list.\nDrawSelectionUnderScrollbar=yes  ; boolean,        enable/disable scroll bar, default value is `true`.\nColumnWidthN=                    ; integer,        the default columns width in pixels. `N` is integer column index.\nColumnX=                         ; string:integer, the column definition. `string` is a column header text. \n                                 ;                 `integer` is a column width in pixels. `X` is an any text.\nListBoxYAttribute:Attrname=Value ; string,         allows setting list box attributes. `Attrname` is column attribute.\n                                 ;                 `Value` is column attribute value.\n```\n\n#### [XNATrackbar](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATrackbar.cs)\n\n_(inherits [XNAPanel](#XNAPanel))_\n\n```ini\n[SOMETRACKBAR] ; XNATrackbar\nMinValue=0     ; integer, the minumum value available for XNATrackbar.\nMaxValue=10    ; integer, the maximum value available for XNATrackbar.\nValue=0        ; integer, the default value available for XNATrackbar.\nClickSound=    ; string,  loads a sound with the specific file name with suffix as XNATrackbar click sound.\n```\n\n#### [XNALabel](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNALabel.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMELABEL]            ; XNALabel\nRemapColor=255,255,255 ; color,    same as XNAControl's `RemapColor`.\nTextColor=196,196,196  ; color,    determine color of the text in label.\nFontIndex=0            ; integer,  the index of font loaded from font list.\nAnchorPoint=0.0,0.0    ; 2 floats, this will set a label's text start drawing point.\nTextShadowDistance=0.1 ; float,    the distance between text and its shadow.\nTextAnchor=            ; enum (NONE | LEFT | RIGHT | HORIZONTAL_CENTER | TOP | BOTTOM | VERTICAL_CENTER),\n                       ;           this will set a text anchor in label draw box.\n```\n\n#### [XNAButton](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAButton.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMEBUTTON]               ; XNAButton\nTextColorIdle=255,255,255  ; color,   the text color when cursor isn't above the button.\nTextColorHover=255,255,255 ; color,   the text color when cursor above the button.\nHoverSoundEffect=          ; string,  loads a sound with the specific file name with suffix as button hover sound.\nClickSoundEffect=          ; string,  loads a sound with the specific file name with suffix as button click sound.\nAdaptiveText=true          ; boolean, specifies how the client should change the start text drawing position \n                           ;          in the button to fill all the free space. Default value is `true`.\nAlphaRate=0.01             ; float,   the button's transparency changing rate per 100 milliseconds. \n                           ;          If the button is transparent, it'll become non-transparent at this rate. \nFontIndex=0                ; integer, the index of loaded from font list.\nIdleTexture=               ; string,  loads a texture with the specific file name with suffix as button idle texture.\nHoverTexture=              ; string,  loads a texture with the specific file name with suffix as button hover texture.\nTextShadowDistance=0.1     ; float,   the distance between text and its shadow.\n```\n\n#### [XNAClientButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientButton.cs)\n\n_(inherits [XNAButton](#XNAButton))_\n\n```ini\n[SOMECLIENTBUTTON] ; XNAClientButton\nMatchTextureSize=  ; boolean, the button's width and height will match its texture properties. \nToolTip=           ; text,    the tooltip for button.\n```\n\n#### [XNAClientToggleButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientToggleButton.cs)\n\n_(inherits [XNAButton](#XNAButton))_\n\n```ini\n[SOMECLIENTTOGGLEBUTTON] ; XNAClientToggleButton\nCheckedTexture=          ; string, loads a texture with the specific file name with suffix as toggle button checked texture.\nUncheckedTexture=        ; string, loads a texture with the specific file name with suffix as toggle button unchecked texture.\nToolTip=                 ; text, the tooltip for toggle button.\n```\n\n#### [XNALinkButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNALinkButton.cs)\n\n_(inherits [XNAClientButton](#XNAClientButton))_\n\n```ini\n[SOMELINKBUTTON] ; XNALinkButton\nURL=             ; string, the URL-link for OS Windows.\nUnixURL=         ; string, the URL-link for Unix-like OS.\nArguments=       ; string, the arguments separated with space for URL-link.\n```\n\n#### [XNACheckbox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNACheckBox.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMECHECKBOX]             ; XNACheckbox\nFontIndex=0                ; integer, the index of font loaded from font list.\nIdleColor=196,196,196      ; color,   the the text color when cursor isn't above the checkbox.\nHighlightColor=255,255,255 ; color,   the text color when cursor above the checkbox.\nAlphaRate=0.1              ; float,   the checkbox's transparency changing rate per 100 milliseconds. \n                           ;          If the checkbox is transparent, it'll become non-transparent at this rate. \nAllowChecking=true         ; boolean, the allows user to check/uncheck checkbox.\nChecked=true               ; boolean, the default checkbox status.\n```\n\n#### [XNAClientCheckbox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientCheckBox.cs)\n\n_(inherits [XNACheckBox](#XNACheckbox))_\n\n```ini\n[SOMECLIENTCHECKBOX] ; XNAClientCheckbox\nToolTip=             ; text, the tooltip for checkbox.\n```\n\n#### [XNADropDown](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNADropDown.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMEDROPDOWN]                  ; XNADropDown\nOpenUp=false                    ; boolean, defines open/close default status.\nDropDownTexture=                ; string,  loads a texture with the specific file name with suffix as \n                                ;          texture when dropdown closed.\nDropDownOpenTexture=            ; string,  loads a texture with the specific file name with suffix as \n                                ;          texture when dropdown opened.\nItemHeight=17                   ; integer, the height of each dropdown item in pixels.\nClickSoundEffect=               ; string,  loads a sound with the specific file name with suffix as \n                                ;          dropdown click sound.\nFontIndex=0                     ; integer, the index of font loaded from font list.\nBorderColor=196,196,196         ; color,   the color for dropdown's border line when it open.\nFocusColor=64,64,64             ; color,   the color for dropdown item when cursore above it.\nBackColor=0,0,0                 ; color,   the background color dropdown when it open.\nDisabledItemColor=169,169,169   ; color,   the color for disabled dropdown item.\nOptionX=                        ; string,  the text option for dropdown. `X` is an any text that helps to \n                                ;          describe this option e.g., `Option_FirstOption`.\n; Option_FirstOption=1\n; Option_SecondOption=two\n; Option_ThirdOption=33333\n```\n\n#### [XNAClientDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientDropDown.cs)\n\n_(inherits XNADropDown)_\n\n```ini\n[SOMECLIENTDROPDOWN] ; XNAClientDropDown\nToolTip=            ; text, tooltip for dropdown.\n```\n\n#### [XNAClientColorDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientColorDropDown.cs)\n\n_(inherits XNAClientDropDown)_\n\n```ini\n[SOMECOLORDROPDOWN] ; XNAClientColorDropDown\nItemsDrawMode=TextAndIcon         ; enum (Text | Icon | TextAndIcon),\n                                  ; this will set what combination of texture and text should client use.\nRandomColorTexture=randomicon.png ; string, the file to load as texture for random color.\nDisabledItemTexture=              ; string, the file to load as texture for disabled items, defaults to texture generated from disabled item color\nColorTextureHeight=               ; int, color icon height in pixels.\nColorTextureWidth=                ; int, color icon width in pixels.\n```\n\n\n#### [XNATabControl](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATabControl.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMETABCONTROL]              ; XNATabControl\nRemapColor=255,255,255        ; color,   the tab text color.\nTextColor=255,255,255         ; color,   the tab text color.\nTextColorDisabled=169,169,169 ; color,   the color for disabled tab.\nRemoveTabIndexN=false         ; boolean, `N` is `integer` equivalent of tab index.\n\n; RemoveTabIndex0=true\n```\n\n#### [XNATextBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATextBox.cs)\n\n_(inherits [XNAControl](#XNAControl))_\n\n```ini\n[SOMETEXTBOX]                ; XNATextBox\nMaximumTextLength=2147483647 ; integer, set maximum input string length.\n```\n\n#### [XNASuggestionTextBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNASuggestionTextBox.cs)\n\n_(inherits [XNATextBox](#XNATextBox))_\n\n```ini\n[SOMESUGGESTIONTEXTBOX] ; XNASuggestionTextBox\nSuggestion=             ; string, set default background text when no text has typed.\n```\n\n### Basic Control Property Examples\n\n```ini\n[lblExample]\nX=100\nY=100\nText=Text Sample\nToolTip=Big and beautiful tooltip@that help to undestand lblExample.\nTextColor=255,255,255\nSize=100,100\nVisible=yes\nEnabled=false\nDistanceFromRightBorder=10\nDistanceFromLeftBorder=10\nFillWidth=10\nFillHeight=10\n```\n\n### Special Controls & Their Properties\n\nSome controls are only available under specific circumstances.\n\n#### CoopBriefingBox\n\n```ini\n; GameLobbyBase.ini\n\n[MapPreviewBox_CoopBriefingBox]\nFontIndex=0\n```\n\n#### GameLobbyBase Controls\n\nFollowing controls are only available as children of `GameLobbyBase` and derived controls.\n\n##### [GameSessionCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/GameSessionCheckBox.cs)\n\n_(inherits [XNAClientCheckBox](#XNAClientCheckBox))_\n\nGame option checkbox for the game lobby. Supports broadcasting game options to the CnCNet lobby and displaying them in the game list and filters.\n\n```ini\n[SOMEGAMESESSIONCHECKBOX]                  ; GameSessionCheckBox\nSpawnIniOption=                            ; string,  spawn INI option to set when checked/unchecked.\nEnabledSpawnIniValue=True                  ; string,  spawn INI value when checkbox is checked. Defaults to `True`.\nDisabledSpawnIniValue=False                ; string,  spawn INI value when checkbox is unchecked. Defaults to `False`.\nCustomIniPath=                             ; string,  custom INI path for map-specific settings.\nReversed=false                             ; boolean, reverse the checkbox behavior.\nChecked=false                              ; boolean, initial checked state.\nMapScoringMode=Irrelevant                  ; enum (Irrelevant | DenyWhenChecked | DenyWhenUnchecked),\n                                           ;          controls whether the setting affects map scoring.\nBroadcastToLobby=false                     ; boolean, include this checkbox in the GAME broadcast to CnCNet lobby.\nShowInGameList=false                       ; boolean, show icon/text in the game list.\nShowInGameListOnRight=false                ; boolean, show icon on the right side of the game list. Only applies if \n                                           ;          `ShowInGameList` is `true`.\nShowInGameInformationPanel=false           ; boolean, show icon/text in the game information panel.\nShowInGameInformationPanelAsIconOnly=false ; boolean, show only the icon in the game information panel. Only applies if \n                                           ;          `ShowInGameInformationPanel` is `true`.\nShowIconInGameLobby=false                  ; boolean, show icon in the game lobby control.\nShowInFilters=false                        ; boolean, show this setting in the filters panel for game filtering.\nEnabledIcon=                               ; string,  texture name for the icon when setting is enabled.\nDisabledIcon=                              ; string,  texture name for the icon when setting is disabled.\nSortOrder=0                                ; integer, display order for icons in GameInformationPanel and GameListBox. \n                                           ;          Lower values appear first.\n```\n\n##### [CampaignCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignCheckBox.cs)\n\n_(inherits [GameSessionCheckBox](#GameSessionCheckBox))_\n\nUse this control type for campaign checkboxes in `CampaignSelector.ini`. Inherits all properties from `GameSessionCheckBox`. Additional properties for this control type are shown below.\n\n```ini\n[SOMECAMPAIGNCHECKBOX]                  ; CampaignCheckBox\nResetToDefaultOnGameExit=false          ; boolean, reset the checkbox to default value when the game exits.\n```\n\n##### [GameLobbyCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyCheckBox.cs)\n\n_(inherits [GameSessionCheckBox](#GameSessionCheckBox))_\n\nUse this control type for game lobby checkboxes in `GameLobbyBase.ini`. Inherits all properties from `GameSessionCheckBox`.\n\n##### [GameSessionDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/GameSessionDropDown.cs)\n\n_(inherits [XNAClientDropDown](#XNAClientDropDown))_\n\nGame option dropdown for the game lobby. Supports broadcasting game options to the CnCNet lobby and displaying them in the game list and filters.\n\n```ini\n[SOMEGAMESESSIONDROPDOWN]                  ; GameSessionDropDown\nItems=                                     ; comma-separated strings,\n                                           ;          comma-separated list of item values for the dropdown.\nItemLabels=                                ; comma-separated strings,\n                                           ;          optional comma-separated list of display labels for items.\nSpawnIniOption=                            ; string,  spawn INI option to set based on selected item.\nDefaultIndex=0                             ; integer, default selected item index.\nDataWriteMode=STRING                       ; enum (STRING | INDEX | BOOLEAN | MAPCODE),\n                                           ;          determines how the value is written to spawn INI.\nOptionName=                                ; string,  display name for this option.\nBroadcastToLobby=false                     ; boolean, include this dropdown in the GAME broadcast to CnCNet lobby.\nShowInGameList=false                       ; boolean, show icon/text in the game list.\nShowInGameListOnRight=false                ; boolean, show icon on the right side of the game list. Only applies if \n                                           ;          `ShowInGameList` is `true`.\nShowInGameInformationPanel=false           ; boolean, show icon/text in the game information panel.\nShowInGameInformationPanelAsIconOnly=false ; boolean, show only the icon in the game information panel. Only applies if \n                                           ;          `ShowInGameInformationPanel` is `true`.\nShowIconInGameLobby=false                  ; boolean, show icon in the game lobby control.\nShowInFilters=false                        ; boolean, show this setting in the filters panel for game filtering.\nIcons=                                     ; comma-separated strings,\n                                           ;          texture names for the icons for each dropdown option. Should match the \n                                           ;          number of items.\nSortOrder=0                                ; integer, display order for icons in GameInformationPanel and GameListBox. \n                                           ;          Lower values appear first.\n```\n\n##### [CampaignDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignDropDown.cs)\n\n_(inherits [GameSessionDropDown](#GameSessionDropDown))_\n\nUse this control type for campaign dropdowns in `CampaignSelector.ini`. Inherits all properties from `GameSessionDropDown`.\n\n##### [GameLobbyDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs)\n\n_(inherits [GameSessionDropDown](#GameSessionDropDown))_\n\nUse this control type for game lobby dropdowns in `GameLobbyBase.ini`. Inherits all properties from `GameSessionDropDown`.\n\n#### XNAOptionsPanel Controls\n\nFollowing controls are only available as children of `XNAOptionsPanel` and derived controls. These currently use basic control properties only.\n\n##### [SettingCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/SettingCheckBox.cs)\n\n_(inherits [XNAClientCheckBox](#XNAClientCheckBox))_\n\n```ini\n[SOMESETTINGCHECKBOX]            ; SettingCheckBox\nDefaultValue=false               ; boolean, default state of the checkbox. Value of `Checked` will be used \n                                 ;          if it is set and this isn't. Otherwise defaults to `false`.\nSettingSection=CustomSettings    ; string,  name of the section in settings INI the setting is saved to. \nSettingKey=                      ; string,  name of the key in settings INI the setting is saved to. \n                                 ;          Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, \n                                 ;          otherwise `CONTROLNAME_Checked`.\nWriteSettingValue=true           ; boolean, enable to write a specific string value to setting INI key \n                                 ;          instead of the checked state of the checkbox. Defaults to `false`.\nEnabledSettingValue=             ; string,  value to write to setting INI key if `WriteSettingValue` \n                                 ;          is set and checkbox is checked.\nDisabledSettingValue=            ; string,  value to write to setting INI key if `WriteSettingValue` \n                                 ;          is set and checkbox is not checked.\nRestartRequired=false            ; boolean, whether or not this setting requires restarting the client to apply. \nParentCheckBoxName=              ; string,  name of a `XNAClientCheckBox` control to use as a parent checkbox \n                                 ;          that is required to either be checked or unchecked, depending on value \n                                 ;          of ParentCheckBoxRequiredValue for this checkbox to be enabled. \n                                 ;          Only works if name can be resolved to an existing control belonging\n                                 ;          to same parent as current checkbox.\nParentCheckBoxRequiredValue=true ; boolean, state required from the parent checkbox for this one to be enabled.\n```\n\n##### [FileSettingCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/FileSettingCheckBox.cs)\n\n_(inherits [XNAClientCheckBox](#XNAClientCheckBox))_\n\n```ini\n[SOMEFILESETTINGCHECKBOX]        ; FileSettingCheckBox\nDefaultValue=false               ; boolean, default state of the checkbox. Value of `Checked` \n                                 ;          will be used if it is set and this isn't. Otherwise defaults to `false`.\nSettingSection=                  ; string,  name of the section in settings INI the setting is saved to.\n                                 ;          Defaults to `CustomSettings`.\nSettingKey=                      ; string,  name of the key in settings INI the setting is saved to.\n                                 ;          Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set,\n                                 ;          otherwise `CONTROLNAME_Checked`.\nRestartRequired=false            ; boolean, whether or not this setting requires restarting the client to apply. \nParentCheckBoxName=              ; string,  name of a `XNAClientCheckBox` control to use as a parent checkbox that \n                                 ;          is required to either be checked or unchecked, depending on value of \n                                 ;          `ParentCheckBoxRequiredValue` for this checkbox to be enabled. \n                                 ;          Only works if name can be resolved to an existing control belonging\n                                 ;          to same parent as current checkbox.\nParentCheckBoxRequiredValue=true ; boolean, state required from the parent checkbox for this one to be enabled.\nCheckAvailability=false          ; boolean, if set, whether or not the checkbox can be (un)checked depends on if \n                                 ;          the files to copy are actually present.\nResetUnavailableValue=false      ; boolean, if set together with `CheckAvailability`, checkbox set to a value that \n                                 ;          is unavailable will be reset back to `DefaultValue`.\nEnabledFileN=                    ; comma-separated strings, \n                                 ;          files to copy if checkbox is checked.\n                                 ;          `N` starts from 0 and is incremented by 1 until no value is found. \n                                 ;          Expects 2-3 comma-separated strings in following format: \n                                 ;          source path relative to game root folder, destination path \n                                 ;          relative to game root folder and a file operation option \n                                 ;          (see #appendix-file-operation-options).\nDisabledFileN=                   ; comma-separated strings, \n                                 ;          files to copy if checkbox is not checked. \n                                 ;          `N` starts from 0 and is incremented by 1 until no value is found. \n                                 ;          Expects 2-3 comma-separated strings in following format: \n                                 ;          source path relative to game root folder, destination path\n                                 ;          relative to game root folder and a file operation option \n                                 ;          (see #appendix-file-operation-options).\n```\n\n##### [SettingDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/SettingDropDown.cs)\n\n_(inherits [XNAClientDropDown](#XNAClientDropDown))_\n\n```ini\n[SOMESETTINGDROPDOWN]  ; SettingDropDown\nItems=                 ; comma-separated strings,\n                       ;          comma-separated list of strings to include as items to display on the dropdown control.\nDefaultValue=0         ; integer, default item index of the dropdown.\nSettingSection=        ; string,  name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`.\nSettingKey=            ; string,  name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` \n                       ;          if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`.\nWriteItemValue=false   ; boolean, enable to write selected item value to the setting INI key instead of the \n                       ;          checked state of the checkbox.\nRestartRequired=true   ; boolean, whether or not this setting requires restarting the client to apply.\n```\n\n##### [FileSettingDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/FileSettingDropDown.cs)\n\n_(inherits [XNAClientDropDown](#XNAClientDropDown))_\n\n```ini\n[SOMEFILESETTINGDROPDOWN]            ; FileSettingDropDown\nItems=                               ; comma-separated strings,\n                                     ;          comma-separated list of strings to include as items\n                                     ;          to display on the dropdown control.\nDefaultValue=0                       ; integer, default item index of the dropdown.\nSettingSection=CustomSettings        ; string,  name of the section in settings INI the setting is saved to.\nSettingKey=CONTROLNAME_SelectedIndex ; string,  name of the key in settings INI the setting is saved to. \nRestartRequired=false                ; boolean, whether or not this setting requires restarting the client to apply.\nResetUnavailableValue=false          ; boolean, determines if the client would adjust the setting value automatically\n                                     ;          if the current value becomes unavailable.\nItemXFileN=                          ; comma-separated strings, \n                                     ;          files to copy when dropdown item `X` is selected. \n                                     ;          `N` starts from 0 and is incremented by 1 until no value is found. \n                                     ;          Expects 2-3 comma-separated strings in following format: \n                                     ;          source path relative to game root folder,\n                                     ;          destination path relative to game root folder and a file operation option \n                                     ;          (see #appendix-file-operation-options).\n```\n\n##### Appendix: File Operation Options\n\nValid file operation options available for files defined for `FileSettingCheckBox` and `FileSettingDropDown` are as follows:\n\n- `AlwaysOverwrite`: Always overwrites the destination file with source file.\n- `OverwriteOnMismatch`: Overwrites the destination file with source file only if they are different.\n- `DontOverwrite`: Never overwrites the destination file with source file if destination file is already present.\n- `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.\n- `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.\n\n### Dynamic Control Properties\n\nDynamic Control Properties CAN use constants.\n\nThese can ONLY be used in parent controls that inherit the `INItializableWindow` class.\n\n```ini\n$X=10            ; integer, the X location of the control  \n$Y=20            ; integer, the Y location of the control  \n$Width=50        ; integer, the Width of the control  \n$Height=10       ; integer, the Height of the control  \n$TextAnchor=LEFT ; enum (NONE | LEFT | RIGHT | HORIZONTAL_CENTER | TOP | BOTTOM | VERTICAL_CENTER),\n                 ;          this will set a text anchor in label draw box.\n```\n\n### Dynamic Control Property Examples\n\n```ini\n[lblExample]\n$X=100\n$X=MY_X_CONSTANT\n$Y=100\n$Y=MY_Y_CONSTANT\n$Width=100\n$Width=MY_WIDTH_CONSTANT\n$Height=100\n$Height=MY_HEIGHT_CONSTANT\n```\n\n## Window Properties\n\nChildren of [XNAWindow](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAWindow.cs) that define their own properties.\n\n### [LoadingScreen](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/LoadingScreen.cs)\n\n```ini\n; LoadingScreen.ini\n[LoadingScreen]\nRandomBackgroundTextures=  ; comma-separated list of strings,\n                           ; paths of files to use randomly as BackgroundTexture\n```\n\n## [CampaignSelector](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs)\n\n### [pnlMissionPreview](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs)\n\n_(inherits [XNAPanel](#XNAPanel))_\n\nYou can now set the preview image for each mission in the campaign selector, known as the mission preview panel.\n\nTo 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`.\n\nTo adjust panel size and position, modify `pnlMissionPreview` in `CampaignSelector.ini`. Inherits all properties from `XNAPanel`.\n\n```ini\n[pnlMissionPreview]          ; XNAPanel\n...\n```\n\nTo 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.\n\nIn `Battle.ini`:\n```ini\n[YourMissionSection]\nPreviewImage= ; string, path to the image file relative to the `Resources/Mission Previews` folder to use as mission preview image.\n```\n\nIf `PreviewImage` property is not set for a mission, `Resources/Mission Previews/Default.png` will be used as default.\n\n### [tbMissionDescription](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs)\n\n_(inherits [XNATextBlock](#XNATextBlock))_\n\nThis 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.\n\nTo adjust the text block size and position, modify `tbMissionDescription` in `CampaignSelector.ini`. Inherits all properties from `XNATextBlock`.\n\n```ini\n[tbMissionDescription]       ; XNATextBlock\n...\n```\n\n# Global Config Files\n\n## [ClientDefinition](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientCore/ClientConfiguration.cs)\n> [!NOTE]\n> _TODO work in progress_\n\nThe `ClientDefinitions.ini` file defines the client's global settings, including the game type, recommended resolutions and the executable file used to launch the game.\n\nIn `ClientDefinitions.ini`:\n```ini\n[Settings]\nTrustedDomains=                ; comma-separated list of strings,\n                               ; domain names to match links and prevent the message box from appearing before they open by default browser\n                               ; example: cncnet.org,github.com,moddb.com\n```\n\n```ini\n[Settings]\nSaveSkirmishGameOptions=false  ; boolean, whether or not previously used game options in skirmish are saved across client sessions\nSaveCampaignGameOptions=false  ; boolean, whether or not previously used game options in campaign are saved across client sessions\n```\n\n```ini\n[Settings]\nCustomMissionPath=Maps/CustomMissions ; path to the folder containing fan-made maps\nCustomMissionSupplementFile0Extension=csf ; extension of the first supplement file\nCustomMissionSupplementFile0CopyAs=stringtable99.csf ; target filename for the first supplement file (required if Extension is present)\nCustomMissionSupplementFile1Extension=pal ; extension of the second supplement file\nCustomMissionSupplementFile1CopyAs=custommission.pal ; target filename for the second supplement file (required if Extension is present)\nCustomMissionSupplementFile2Extension=shp ; extension of the third supplement file\nCustomMissionSupplementFile2CopyAs=custommission.shp ; target filename for the third supplement file (required if Extension is present)\n; supplement files that are supposed to be copied to the game folder when a custom mission is played\n; the iteration stops if a number is missing (e.g., if File3Extension is missing, only File0, File1, and File2 are processed)\n; both Extension and CopyAs must be provided for each file number; each Extension value must be unique - duplicate extensions are not allowed\n```\n\n```ini\n[Settings]\nReturnToMainMenuOnMissionLaunch=true ; whether or not client returns to main menu when launching a mission\n```\n\n```ini\n[Settings]\nCampaignTagSelectorEnabled=false ; turns on the campaign tag selector, showing a window to let users choose which group of missions to play\n```\n\n```ini\n[Settings]\nCompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe ; comma-separated list of strings, to check for DirectDraw compatibility mode issues\n```\n\n```ini\n[Settings]\nShowGameIconInGameList=true ; boolean, whether to show the game icon in the game listing. Defaults to true.\n```\n"
  },
  {
    "path": "Docs/Migration-INI.md",
    "content": "# Migrating from older versions - INI configuration\n\nMigrating to client version [2.11.0.0][client_2.11] or [2.12.0][client_2.12] from pre-2.11.0.0.\n\nThis 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.\n\nIt is **highly recommended** to make a complete backup of your game/mod before starting.\n\n## Edit `ClientDefinitions.ini`\n\nSince v2.12, the client has unified different builds among game types. The game type must be defined in `ClientDefinitions.ini` now.\n\n- Add `[Settings]->ClientGameType=YR` (defines client behaviour by game. Allowed options: TS, YR, Ares)\n\nThe way the client is launched on Unix systems has changed.\n\n1. Add `[Settings]->UnixLauncherExe=YRLauncher.sh` (script file name can be anything)\n2. Create `YRLauncher.sh` in game directory:\n\n```sh\n#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\ndotnet Resources/BinariesNET8/UniversalGL/clientogl.dll \"$@\"\n```\n\n3. **OPTIONAL** Add these entries in `[Settings]` (fill with your required/forbidden mod files):\n\n```ini\n; Comma-separated list of files required to run the game / mod that are not included in the installation.\nRequiredFiles=\n; Comma-separated list of files that cannot be present to run the game / mod without problems.\nForbiddenFiles=\n```\n\n## Add `GameLobbyBase.ini`\n\nUnlike 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**.\n\nSee example configuration below or in [YR mod base][mod_base]:\n\n<details>\n<summary>Click to show file content</summary>\n\n```ini\n[INISystem]\nBasedOn=GenericWindow.ini\n\n[GameLobbyBase]\nPlayerOptionLocationX=12        ; def=25\nPlayerOptionLocationY=25        ; def=24\nPlayerOptionVerticalMargin=9    ; def=12\nPlayerOptionHorizontalMargin=5  ; def=3\nPlayerOptionCaptionLocationY=6  ; def=6\nPlayerNameWidth=127             ; def=136\nSideWidth=120                   ; def=91\nColorWidth=80                   ; def=79\nStartWidth=50                   ; def=49\nTeamWidth=50                    ; def=46\n\n; controls\n$CC00=btnLaunchGame:GameLaunchButton\n$CC01=btnLeaveGame:XNAClientButton\n$CC03=MapPreviewBox:MapPreviewBox\n$CC04=GameOptionsPanel:XNAPanel\n$CC05=PlayerOptionsPanel:XNAPanel\n$CC06=lblMapName:XNALabel\n$CC07=lblMapAuthor:XNALabel\n$CC08=lblGameMode:XNALabel\n$CC09=lblMapSize:XNALabel\n$CC12=lbMapList:XNAMultiColumnListBox\n$CC13=ddGameMode:XNAClientDropDown\n$CC14=lblGameModeSelect:XNALabel\n$CC15=btnPickRandomMap:XNAClientButton\n$CC16=tbMapSearch:XNASuggestionTextBox\n$CC17=PlayerExtraOptionsPanel:PlayerExtraOptionsPanel\n$CC18=lbChatMessages:ChatListBox\n$CC19=tbChatInput:XNAChatTextBox\n$CC20=panelBorderTop:XNAExtraPanel\n$CC21=panelBorderBottom:XNAExtraPanel\n$CC22=panelBorderLeft:XNAExtraPanel\n$CC23=panelBorderRight:XNAExtraPanel\n$CC24=panelBorderCornerTL:XNAExtraPanel\n$CC25=panelBorderCornerTR:XNAExtraPanel\n$CC26=panelBorderCornerBL:XNAExtraPanel\n$CC27=panelBorderCornerBR:XNAExtraPanel\n\n[lblName]\nText=Players; in the game its Players, makes more sense than Name actually, eh\n\n[lblSide]\nText=Side\n\n[lblStart]\nText=Start\nVisible=true\n\n[lblColor]\nText=Color\n\n[lblTeam]\nText=Team\n\n[ddPlayerStartBase]\nEnabled=true\nVisible=true\n\n[ddPlayerStart0]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart1]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart2]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart3]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart4]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart5]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart6]\n$BaseSection=ddPlayerStartBase\n\n[ddPlayerStart7]\n$BaseSection=ddPlayerStartBase\n\n[lbMapList]\n$X=LOBBY_EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP + 33\n$Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + LOBBY_EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING))\n$Height=getBottom(MapPreviewBox) - getY($Self)\nSolidColorBackgroundTexture=0,0,0,128\n\n[ddGameMode]\n$Width=180\n$Height=DEFAULT_CONTROL_HEIGHT\n$X=getRight(lbMapList) - getWidth($Self)\n$Y=getY(lbMapList) - getHeight($Self) - EMPTY_SPACE_TOP\n\n[lblGameModeSelect]\nText=Game mode:\n$X=getX(ddGameMode) - getWidth($Self) - LABEL_SPACING\n$Y=getY(ddGameMode) + 1\n\n[btnMapSortAlphabetically]\nVisible=false\nEnabled=false\n\n[btnLaunchGame]\nText=Launch Game\n$Width=BUTTON_WIDTH_133\n$Height=DEFAULT_BUTTON_HEIGHT\n$X=LOBBY_EMPTY_SPACE_SIDES\n$Y=getHeight($ParentControl) - getHeight($Self) - EMPTY_SPACE_BOTTOM\n\n[btnPickRandomMap]\nText=Pick Random Map\n$Width=BUTTON_WIDTH_133\n$Height=DEFAULT_BUTTON_HEIGHT\n$X=LOBBY_EMPTY_SPACE_SIDES\n$Y=getY(btnLaunchGame) - getHeight($Self) - LOBBY_PANEL_SPACING\n\n[tbMapSearch]\nSuggestion=Search map...\n$Width=getRight(lbMapList) - getRight(btnPickRandomMap) - LOBBY_PANEL_SPACING\n$Height=DEFAULT_BUTTON_HEIGHT ;DEFAULT_CONTROL_HEIGHT\n$X=getRight(btnPickRandomMap) + LOBBY_PANEL_SPACING\n$Y=getY(btnPickRandomMap) ; + 1\nBackColor=255,255,255\n;SolidColorBackgroundTexture=0,0,0,128\n\n[MapPreviewBox]\n$Width=812\n$Height=380\n$X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES\n$Y=292\nSolidColorBackgroundTexture=0,0,0,128\n\n[lblMapName]\n$Height=DEFAULT_LBL_HEIGHT\n$X=getX(MapPreviewBox)\n$Y=getBottom(MapPreviewBox) + LABEL_SPACING\n\n[lblMapAuthor]\n$TextAnchor=LEFT\n$AnchorPoint=getRight(MapPreviewBox),getY(lblMapName)\n\n[lblGameMode]\n$Height=DEFAULT_LBL_HEIGHT\n$X=getX(lblMapName)\n$Y=getBottom(lblMapName) + LABEL_SPACING\n\n[lblMapSize]\n$Height=DEFAULT_LBL_HEIGHT\n$X=getX(lblGameMode)\n$Y=getBottom(lblGameMode) + LABEL_SPACING\n\n[btnLeaveGame]\nText=Leave Game\n$Width=BUTTON_WIDTH_133\n$Height=DEFAULT_BUTTON_HEIGHT\n$X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES\n$Y=getY(btnLaunchGame)\n\n[PlayerOptionsPanel]\n$X=getX(MapPreviewBox)\n$Y=EMPTY_SPACE_TOP\n$Width=getWidth($ParentControl) - (getX($Self) + (getWidth(GameOptionsPanel) + LOBBY_EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING))\n$Height=getHeight(GameOptionsPanel)\nSolidColorBackgroundTexture=0,0,0,128\n\n$CC00=btnPlayerExtraOptionsOpen:XNAClientButton\n\n[PlayerExtraOptionsPanel]\n$Width=238\n$Height=247\n$X=getRight(PlayerOptionsPanel) - getWidth($Self)\n$Y=getY(PlayerOptionsPanel)\nSolidColorBackgroundTexture=0,0,0,128\n\n[btnPlayerExtraOptionsOpen]\n$Width=OPEN_BUTTON_WIDTH\n$Height=OPEN_BUTTON_HEIGHT\n$X=getWidth($ParentControl) - getWidth($Self)\n$Y=0\nIdleTexture=optionsButton.png\nHoverTexture=optionsButton_c.png\n\n[GameOptionsPanel]\n$Width=330\n$Height=270\n$X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP\nSolidColorBackgroundTexture=0,0,0,128\n\n; Left column checkboxes\n$CC-GO01=chkShortGame:GameLobbyCheckBox\n$CC-GO02=chkRedeplMCV:GameLobbyCheckBox\n$CC-GO03=chkSuperWeapons:GameLobbyCheckBox\n$CC-GO04=chkCrates:GameLobbyCheckBox\n$CC-GO05=chkBuildOffAlly:GameLobbyCheckBox\n$CC-GO06=chkMultiEngineer:GameLobbyCheckBox\n\n; Right column checkboxes\n$CC-GO07=chkIngameAllying:GameLobbyCheckBox\n$CC-GO08=chkStolenTech:GameLobbyCheckBox\n$CC-GO09=chkNavalCombat:GameLobbyCheckBox\n$CC-GO10=chkDestroyableBridges:GameLobbyCheckBox\n$CC-GO11=chkBrutalAI:GameLobbyCheckBox\n$CC-GO12=chkNoSpawnPreview:GameLobbyCheckBox\n\n; Dropdowns\n$CC-GODD01=cmbCredits:GameLobbyDropDown\n$CC-GODD02=lblCredits:XNALabel\n; $CC-GODD03=cmbGameSpeedCap:GameLobbyDropDown ; different in MP and SP\n$CC-GODD03PH=cmbGameSpeedCapPlaceholder:XNAControl\n$CC-GODD04=lblGameSpeedCap:XNALabel\n$CC-GODD05=cmbTechLevel:GameLobbyDropDown\n$CC-GODD06=lblTechLevel:XNALabel\n$CC-GODD07=cmbStartingUnits:GameLobbyDropDown\n$CC-GODD08=lblStartingUnits:XNALabel\n\n$CC01=BtnSaveLoadGameOptions:XNAClientButton\n\n[BtnSaveLoadGameOptions]\n$Width=OPEN_BUTTON_WIDTH\n$Height=OPEN_BUTTON_HEIGHT\n$X=getWidth($ParentControl) - getWidth($Self)\n$Y=0\nIdleTexture=optionsButton.png\nHoverTexture=optionsButton_c.png\n\n;============================\n; LEFT Column Checkboxes\n;============================\n\n[chkShortGame]\nText=Short Game\nSpawnIniOption=ShortGame\nChecked=True\nToolTip=Players win when all enemy buildings are destroyed.\n$X=EMPTY_SPACE_SIDES\n$Y=EMPTY_SPACE_TOP\n\n[chkRedeplMCV]\nText=MCV Repacks\nSpawnIniOption=MCVRedeploy\nChecked=True\nToolTip=Players have the ability to move their command center after it's deployed.\n$X=getX(chkShortGame)\n$Y=getBottom(chkShortGame) + GAME_OPTION_ROW_SPACING\n\n[chkSuperWeapons]\nText=Superweapons\nSpawnIniOption=Superweapons\nChecked=False\nToolTip=Allow superweapons to be built.\n$X=getX(chkShortGame)\n$Y=getBottom(chkRedeplMCV) + GAME_OPTION_ROW_SPACING\n\n[chkCrates]\nText=Crates Appear\nSpawnIniOption=Crates\nChecked=False\nToolTip=Random power-up crates will appear.\n$X=getX(chkShortGame)\n$Y=getBottom(chkSuperWeapons) + GAME_OPTION_ROW_SPACING\n\n[chkBuildOffAlly]\nText=Build Off Allies\nSpawnIniOption=BuildOffAlly\nChecked=True\nToolTip=Allow players to build near their allies' Construction Yards.\n$X=getX(chkShortGame)\n$Y=getBottom(chkCrates) + GAME_OPTION_ROW_SPACING\n\n[chkMultiEngineer]\nText=Multi Engineer\nSpawnIniOption=MultiEngineer\nChecked=False\nToolTip=Capturing structures requires 3 Engineers instead of 1.\n$X=getX(chkShortGame)\n$Y=getBottom(chkBuildOffAlly) + GAME_OPTION_ROW_SPACING\n\n;============================\n; Right Column Checkboxes\n;============================\n\n[chkIngameAllying]\nText=Ingame Allying\nSpawnIniOption=AlliesAllowed\nCustomIniPath=INI/Game Options/AlliesAllowed.ini\nChecked=True\nToolTip=Players can form and break alliances in game.\n$X=getX(chkShortGame) + GAME_OPTION_COLUMN_SPACING\n$Y=getY(chkShortGame)\n\n[chkStolenTech]\nText=Stolen Tech\nCustomIniPath=INI/Game Options/StolenTech.ini\nChecked=True\nToolTip=Allow infiltration of battle labs for stolen tech infantry.\nReversed=yes\n$X=getX(chkIngameAllying)\n$Y=getBottom(chkIngameAllying) + GAME_OPTION_ROW_SPACING\n\n[chkNavalCombat]\nText=Naval Combat\nCustomIniPath=INI/Game Options/NavalCombat.ini\nChecked=True\nToolTip=Allow shipyards and naval units to be built.\nReversed=yes\n$X=getX(chkIngameAllying)\n$Y=getBottom(chkStolenTech) + GAME_OPTION_ROW_SPACING\n\n[chkDestroyableBridges]\nText=Destroyable Bridges\nCustomIniPath=INI/Game Options/DestroyableBridges.ini\nChecked=True\nLocation=1038,86\nToolTip=Allow bridges to be destroyed by conventional weapons & force firing.\nReversed=yes\n$X=getX(chkIngameAllying)\n$Y=getBottom(chkNavalCombat) + GAME_OPTION_ROW_SPACING\n\n[chkBrutalAI]\nText=Brutal AI\nCustomIniPath=INI/Game Options/BrutalAI.ini\nChecked=False\nLocation=1038,107\nToolTip=Makes the AI harder across all levels.\n$X=getX(chkIngameAllying)\n$Y=getBottom(chkDestroyableBridges) + GAME_OPTION_ROW_SPACING\n\n[chkNoSpawnPreview]\nText=No Spawn Previews\nCustomIniPath=INI/Game Options/NoSpawnPreview.ini\nChecked=True\nLocation=1038,128\nToolTip=Do not display players' starting locations in loading screen map preview.\n$X=getX(chkIngameAllying)\n$Y=getBottom(chkBrutalAI) + GAME_OPTION_ROW_SPACING\n\n\n;============================\n; Dropdowns\n;============================\n\n[lblCredits]\nText=Credits:\n$Height=DEFAULT_LBL_HEIGHT\n$X=getX(cmbCredits)\n$Y=getY(cmbCredits) - LABEL_SPACING - DEFAULT_LBL_HEIGHT\n\n[cmbCredits]\nOptionName=Starting Credits\nItems=50000,45000,40000,35000,30000,25000,20000,15000,10000\nDefaultIndex=7\nSpawnIniOption=Credits\nDataWriteMode=String\nToolTip=Changes how many credits players start with.\n$Width=GAME_OPTION_DD_WIDTH\n$Height=GAME_OPTION_DD_HEIGHT\n$X=EMPTY_SPACE_SIDES\n$Y=getHeight($ParentControl) - (getHeight($Self) + EMPTY_SPACE_BOTTOM)\n\n[lblGameSpeedCap]\nText=Game Speed:\n$Height=DEFAULT_LBL_HEIGHT\n$X=getX(cmbGameSpeedCapPlaceholder)\n$Y=getY(cmbGameSpeedCapPlaceholder) - LABEL_SPACING - DEFAULT_LBL_HEIGHT\n\n[cmbGameSpeedCapPlaceholder]\nVisible=false\nEnabled=false\n$Width=GAME_OPTION_DD_WIDTH\n$Height=GAME_OPTION_DD_HEIGHT\n$X=getX(cmbCredits)\n$Y=getY(lblCredits) - LOBBY_PANEL_SPACING - GAME_OPTION_DD_HEIGHT\n\n; not actually a control in this file, used for inheritance\n[cmbGameSpeedCap]\nOptionName=Game Speed\n; Items= ...\nDefaultIndex=2\nSpawnIniOption=GameSpeed\nDataWriteMode=Index\nToolTip=Changes game speed cap.\n$Width=getWidth(cmbGameSpeedCapPlaceholder)\n$Height=getHeight(cmbGameSpeedCapPlaceholder)\n$X=getX(cmbGameSpeedCapPlaceholder)\n$Y=getY(cmbGameSpeedCapPlaceholder)\n\n[lblTechLevel]\nText=Tech Level:\n$X=getX(cmbTechLevel)\n$Y=getY(cmbTechLevel) - LABEL_SPACING - DEFAULT_LBL_HEIGHT\nEnabled=no\nVisible=no\n\n[cmbTechLevel]\nOptionName=Tech Level\nItems=10,9,8,7,6,5,4,3,2,1\nDefaultIndex=0\nSpawnIniOption=TechLevel\nDataWriteMode=String\nToolTip=Changes maximum tech level for all players.\n$Width=GAME_OPTION_DD_WIDTH\n$Height=GAME_OPTION_DD_HEIGHT\n$X=EMPTY_SPACE_SIDES + GAME_OPTION_COLUMN_SPACING\n$Y=getY(cmbCredits)\nEnabled=no\nVisible=no\n\n[lblStartingUnits]\nText=Starting Units:\n$X=getX(cmbStartingUnits)\n$Y=getY(cmbStartingUnits) - LABEL_SPACING - DEFAULT_LBL_HEIGHT\n\n[cmbStartingUnits]\nOptionName=Starting Units\nItems=10,9,8,7,6,5,4,3,2,1,0\nDefaultIndex=10\nSpawnIniOption=UnitCount\nDataWriteMode=String\nToolTip=Changes how many infantry units players start with.\n$Width=GAME_OPTION_DD_WIDTH\n$Height=GAME_OPTION_DD_HEIGHT\n$X=getX(cmbTechLevel)\n$Y=getY(lblTechLevel) - LOBBY_PANEL_SPACING - GAME_OPTION_DD_HEIGHT\n\n; Window Border Sides\n\n[panelBorderTop]\nLocation=0,-8\nBackgroundTexture=border-top.png\nDrawMode=Stretched\nSize=0,9\nFillWidth=0\n\n[panelBorderBottom]\nLocation=0,999\nBackgroundTexture=border-bottom.png\nDrawMode=Stretched\nSize=0,9\nFillWidth=0\nDistanceFromBottomBorder=-8\n\n[panelBorderLeft]\nLocation=-8,0\nBackgroundTexture=border-left.png\nDrawMode=Stretched\nSize=9,0\nFillHeight=0\n\n[panelBorderRight]\nLocation=999,0\nBackgroundTexture=border-right.png\nDrawMode=Stretched\nSize=9,0\nFillHeight=0\nDistanceFromRightBorder=-8\n\n; Window Border Corners\n\n[panelBorderCornerTL]\nLocation=-8,-8\nBackgroundTexture=border-corner-tl.png\nSize=9,9\n\n[panelBorderCornerTR]\nLocation=999,-8\nBackgroundTexture=border-corner-tr.png\nSize=9,9\nDistanceFromRightBorder=-8\n\n[panelBorderCornerBL]\nLocation=-8,999\nBackgroundTexture=border-corner-bl.png\nSize=9,9\nDistanceFromBottomBorder=-8\n\n[panelBorderCornerBR]\nLocation=999,999\nBackgroundTexture=border-corner-br.png\nSize=9,9\nDistanceFromRightBorder=-8\nDistanceFromRightBorder=-8\nDistanceFromBottomBorder=-8\n```\n\n</details>\n\n### Port custom game options\n\nIf 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.\n\nExample option in `GameOptions.ini` in previous versions:\n\n```ini\n[MultiplayerGameLobby]\nCheckBoxes=chkNewOption...\n\n[SkirmishLobby]\nCheckBoxes=chkNewOption...\n\n[chkNewOption]\nText=My New Option\nCustomIniPath=INI/Game Options/MyNewOption.ini\nToolTip=Enable this new option.\nChecked=False\nLocation=1126,79\n```\n\nExample option in `GameLobbyBase.ini` in the new version:\n\n```ini\n[GameOptionsPanel]\n$CC-GONEW=chkNewOption:GameLobbyCheckBox\n\n[chkNewOption]\nText=My New Option\nCustomIniPath=INI/Game Options/MyNewOption.ini\nToolTip=Enable this new option.\nChecked=False\nLocation=1126,79 ; $X and $Y are recommended instead\n```\n\n## Edit `SkirmishLobby.ini`\n\nThis 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.\n\n```ini\n[INISystem]\nBasedOn=GameLobbyBase.ini\n\n[SkirmishLobby]\n$BaseSection=GameLobbyBase\n\n[GameOptionsPanel]\n$CC-GODD03=cmbGameSpeedCapSkirmish:GameLobbyDropDown\n\n[cmbGameSpeedCapSkirmish]\n$BaseSection=cmbGameSpeedCap\nItems=Fastest (MAX),Faster (60 FPS),Fast (30 FPS),Medium (20 FPS),Slow (15 FPS),Slower (12 FPS),Slowest (10 FPS)\n```\n\n## Edit `MultiplayerGameLobby.ini`\n\nThis 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.\n\n<details>\n<summary>Click to show file content</summary>\n\n```ini\n[INISystem]\nBasedOn=GameLobbyBase.ini\n\n[MultiplayerGameLobby]\n$BaseSection=GameLobbyBase\nPlayerOptionLocationX=36\nPlayerOptionLocationY=25        ; def=24\nPlayerOptionVerticalMargin=9    ; def=12\nPlayerOptionHorizontalMargin=5    ; def=3\nPlayerOptionCaptionLocationY=6    ; def=6\nPlayerStatusIndicatorX=8\nPlayerStatusIndicatorY=0\nPlayerNameWidth=135             ; def=136\nSideWidth=110                    ; def=91\nColorWidth=80                    ; def=79\nStartWidth=45                    ; def=49\nTeamWidth=35                    ; def=46\n\n; controls\n$CCMP00=btnLockGame:XNAClientButton\n$CCMP01=chkAutoReady:XNAClientCheckBox\n\n[lbMapList]\n$Height=291\n\n[btnPickRandomMap]\n$Y=getBottom(lbMapList) + LOBBY_PANEL_SPACING\n\n[tbMapSearch]\n$X=getRight(btnPickRandomMap) + LOBBY_PANEL_SPACING\n$Y=getY(btnPickRandomMap)\n\n[lbChatMessagesBase]\nSolidColorBackgroundTexture=0,0,0,128\n$Width=getWidth(lbMapList)\n$X=LOBBY_EMPTY_SPACE_SIDES\n\n[lbChatMessages_Host]\n$BaseSection=lbChatMessagesBase\n$Y=getBottom(btnPickRandomMap) + LOBBY_PANEL_SPACING\n$Height=getBottom(MapPreviewBox) - (getBottom(btnPickRandomMap) + LOBBY_PANEL_SPACING)\n\n[lbChatMessages_Player]\n$BaseSection=lbChatMessagesBase\n$Y=EMPTY_SPACE_TOP\n$Height=getBottom(MapPreviewBox) - getY($Self)\n\n[tbChatInputBase]\nSuggestion=Type here to chat...\n$Width=getWidth(lbMapList)\n$Height=DEFAULT_CONTROL_HEIGHT\n$X=LOBBY_EMPTY_SPACE_SIDES\n$Y=getBottom(MapPreviewBox) + LOBBY_PANEL_SPACING\n\n[tbChatInput_Host]\n$BaseSection=tbChatInputBase\n\n[tbChatInput_Player]\n$BaseSection=tbChatInputBase\n\n[btnLockGame]\n$Width=BUTTON_WIDTH_133\n$Height=DEFAULT_BUTTON_HEIGHT\n$X=getRight(btnLaunchGame) + LOBBY_PANEL_SPACING\n$Y=getY(btnLaunchGame)\n\n[chkAutoReady]\nText=Auto-Ready\n$X=getRight(btnLaunchGame) + LOBBY_PANEL_SPACING\n$Y=getY(btnLaunchGame) + 2\nEnabled=true\nVisible=true\n\n[GameOptionsPanel]\n$CC-GODD03=cmbGameSpeedCapMultiplayer:GameLobbyDropDown\n\n[cmbGameSpeedCapMultiplayer]\n$BaseSection=cmbGameSpeedCap\nItems=Fastest (60 FPS),Faster (45 FPS),Fast (30 FPS),Medium (20 FPS),Slow (15 FPS),Slower (12 FPS),Slowest (10 FPS)\n```\n\n</details>\n\n## Create `CnCNetGameLobby.ini`\n\nThis file extends the multiplayer game lobby with CnCNet-specific controls, like the change tunnel button. **Remove (or port) previous content of this file.**\n\n```ini\n[INISystem]\nBasedOn=MultiplayerGameLobby.ini\n\n[MultiplayerGameLobby]\n$CCMP99=btnChangeTunnel:XNAClientButton\n\n[btnChangeTunnel]\nText=Change Tunnel\n$Width=BUTTON_WIDTH_133\n$Height=DEFAULT_BUTTON_HEIGHT\n$X=getX(btnLeaveGame) - getWidth($Self) - LOBBY_PANEL_SPACING\n$Y=getY(btnLeaveGame)\n```\n\n## Create `LANGameLobby.ini`\n\nThis stub file can extend the multiplayer lobby with LAN-specifc controls. **Remove (or port) previous content of this file.**\n\n```ini\n[INISystem]\nBasedOn=MultiplayerGameLobby.ini\n```\n\n## Edit `GameOptions.ini`\n\nAfter adding all game lobby options to `GameLobbyBase.ini`, remove them here. Remove `[SkirmishLobby]` and `[MultiplayerGameLobby]` sections, too.\n\n## Edit `GenericWindow.ini`\n\nReplace `[SkirmishLobby]` and `[MultiplayerGameLobby]` with this:\n\n```ini\n[GameLobbyBase]\nBackgroundTexture=gamelobbybg.png\nDrawBorders=true\nSize=1230,750\n```\n\n## Edit `GlobalThemeSettings.ini`\n\nThis 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**.\n\nAdd the following:\n\n```ini\n[ParserConstants]\nDEFAULT_LBL_HEIGHT=12\nDEFAULT_CONTROL_HEIGHT=21\nDEFAULT_BUTTON_HEIGHT=23\n\nBUTTON_WIDTH_133=133\n\nOPEN_BUTTON_WIDTH=18\nOPEN_BUTTON_HEIGHT=22 ;18\n\nEMPTY_SPACE_TOP=12\nEMPTY_SPACE_BOTTOM=12\nEMPTY_SPACE_SIDES=12\nBUTTON_SPACING=12\nLABEL_SPACING=6\nCHECKBOX_SPACING=24\n\nLOBBY_EMPTY_SPACE_SIDES=12\nLOBBY_PANEL_SPACING=10\n\nGAME_OPTION_COLUMN_SPACING=160\nGAME_OPTION_ROW_SPACING=6\nGAME_OPTION_DD_WIDTH=132\nGAME_OPTION_DD_HEIGHT=22\n```\n\n## Create `ManualUpdateQueryWindow.ini`\n\nIt is now possible to force a manual query for game/mod updates, which displays a new window.\n\n```ini\n[INISystem]\nBasedOn=GenericWindow.ini\n\n[btnClose]\nLocation=176,110\n```\n\n## Edit `OptionsWindow.ini`\n\nNew checkboxes have been added in the options window.\n\n1. Add sections:\n\n```ini\n[lblPlayerName]\nLocation=12,195\n\n[tbPlayerName]\nLocation=113,193\n\n[lblNotice]\nLocation=12,220\n\n[btnConfigureHotkeys]\nLocation=12,290\n\n[chkDisablePrivateMessagePopup]\nLocation=12,138\nText=Disable private message pop-ups\n\n[chkAllowGameInvitesFromFriendsOnly]\nLocation=276,68\nText=Only receive game invitations@from friends\n\n[lblAllPrivateMessagesFrom]\nLocation=276,138\n\n[ddAllowPrivateMessagesFrom]\nLocation=470,137\n\n[gameListPanel]\nLocation=0,200\n\n[btnForceUpdate]\n```\n\n2. **OPTIONAL** Add sections:\n\n```ini\n[DisplayOptionsPanelExtraControls]\n0=chkMEDDraw:FileSettingCheckBox\n\n[chkMEDDraw]\nLocation=285,147\nText=Enable DDWrapper for map editor\nToolTip=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. \nEnabledFile0=Resources/Compatibility/DLL/ddwrapper.dll,Map Editor/ddraw32.dll,OverwriteOnMismatch\nEnabledFile1=Resources/Compatibility/Configs/aqrit.cfg,Map Editor/aqrit.cfg,KeepChanges\nDefaultValue=false\nSettingSection=Video\nSettingKey=UseDDWrapperForMapEditor\n```\n\n3. **OPTIONAL (YR+Phobos)** Add sections:\n\n```ini\n[GameOptionsPanelExtraControls]\n; Only available with Phobos\n0=chkTooltipsExtra:SettingCheckBox\n1=chkPrioritySelection:SettingCheckBox\n2=chkBuildingPlacement:SettingCheckBox\n\n[chkTooltipsExtra]\nLocation=24,151, ;12,151\nText=Sidebar Tooltip Descriptions\nToolTip=Enables additional information in sidebar tooltips.\nDefaultValue=true\nParentCheckBoxName=chkTooltips\nParentCheckBoxRequiredValue=true\nSettingSection=Phobos\nSettingKey=ToolTipDescriptions\n\n[chkPrioritySelection]\nLocation=242,54\nText=Mass Selection Filtering\nToolTip=If enabled, non-combat units are not selected if mass-selecting together with combat units.\nDefaultValue=false\nSettingSection=Phobos\nSettingKey=PrioritySelectionFiltering\n\n[chkBuildingPlacement]\nLocation=242,78\nText=Show Building Placement Preview\nToolTip=If enabled, shows a preview image of the building when placing it.\nDefaultValue=false\nSettingSection=Phobos\nSettingKey=ShowBuildingPlacementPreview\n```\n\n## Create new `PlayerExtraOptionsPanel.ini`\n\nA new panel that allows for convenient match setup has been added in the game lobby.\n\n```ini\n[btnClose]\nLocation=220,0\nSize=18,18\n\n[lblHeader]\nLocation=12,6\n\n[chkBoxForceRandomSides]\nLocation=12,28\n\n[chkBoxForceRandomColors]\nLocation=12,50\n\n[chkBoxForceRandomTeams]\nLocation=12,72\n\n[chkBoxForceRandomStarts]\nLocation=12,94\n\n[chkBoxUseTeamStartMappings]\nLocation=12,130\n\n[btnHelp]\nLocation=160,130\n\n[lblPreset]\nLocation=12,156\n\n[ddTeamStartMappingPreset]\nSize=157,21\nLocation=65,154\n\n[teamStartMappingsPanel]\nLocation=12,189\n```\n\n## Appendix\n\nFor 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.\n\n### Update client binary files\n\n1. Replace `clientdx.exe`, `clientogl.exe` and `clientxna.exe` in `Resources` with new files. Compiled `.pdb` and `.config` files are optional.\n2. Replace contents of `Resources/Binaries` with new files. This directory contains the .NET Framework 4.8 version of the client.\n3. **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.\n\nThe `Resources` directory should look like this (omitting configuration files and assets):\n\n```plaintext\n<game dir>/Resources     # override the `Resources` folder to update the client binaries\n├── Binaries             # this folder contains partial .NET 4.8 client files\n├── BinariesNET8         # this folder contains .NET 8.0 client files, where modders can either delete it, or keep it for an experimental cross-platform support\n├── clientdx.exe         # .NET 4.8 client main executable\n├── clientdx.exe.config  # distributed along with `.exe` file. Can be removed but it is better to keep it.\n├── clientdx.pdb         # .pdb file contains debug symbols. It can be either deleted or retained.\n├── clientogl.exe        # .NET 4.8 client main executable\n├── clientogl.exe.config # same as above\n├── clientogl.pdb        # same as above\n├── clientxna.exe        # .NET 4.8 client main executable\n├── clientxna.exe.config # same as above\n└── clientxna.pdb        # same as above\n```\n\n### Update the client launcher\n\nThe 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`.\n\n### Keep the old second-stage updater\n\nThe 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.\n\n### Add new assets\n\nEvery file here can be placed either in `Resources` or in theme directories:\n\n- `favActive.png` and `favInactive.png`, 21x21 pixels\n- `optionsButton.png`, `optionsButton_c.png`, `optionsButtonActive.png`, `optionsButtonActive_c.png`, `optionsButtonClose.png` and `optionsButtonClose_c.png`, 18x18 pixels\n- `questionMark.png` and `questionMark_c.png`, 18x18 pixels\n- `sortAlphaAsc.png`, `sortAlphaDesc.png` and `sortAlphaNone.png`, 21x21 pixels\n- `statusAI.png`, `statusClear.png`, `statusEmpty.png`, `statusError.png`, `statusInProgress.png`, `statusOk.png`, `statusUnavailable.png`, `statusWarning.png`, 21x21 pixels\n\nYou can find example assets in the [YR mod base][mod_base].\n\n[client_2.11]: https://github.com/CnCNet/xna-cncnet-client/releases/tag/2.11.0.0\n[client_2.12]: https://github.com/CnCNet/xna-cncnet-client/releases/tag/2.12.0\n[mod_base]: https://github.com/Starkku/cncnet-client-mod-base\n"
  },
  {
    "path": "Docs/Migration.md",
    "content": "Migrating from older versions\n-----------------------------\n\nThis 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.\n\n> [!NOTE]\n> 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.\n\n## 2.13.0\n\n- `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]`.\n\n## 2.12.12\n\n- 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:\n\n  In `updateexec`:\n  ```ini\n  [Delete]\n  ; append those lines in the section\n  Resources\\Binaries\\Windows\\DTAConfig.dll\n  Resources\\Binaries\\Windows\\DTAConfig.pdb\n  Resources\\Binaries\\OpenGL\\DTAConfig.dll\n  Resources\\Binaries\\OpenGL\\DTAConfig.pdb\n  Resources\\Binaries\\XNA\\DTAConfig.dll\n  Resources\\Binaries\\XNA\\DTAConfig.pdb\n  Resources\\BinariesNET8\\Windows\\DTAConfig.dll\n  Resources\\BinariesNET8\\Windows\\DTAConfig.pdb\n  Resources\\BinariesNET8\\OpenGL\\DTAConfig.dll\n  Resources\\BinariesNET8\\OpenGL\\DTAConfig.pdb\n  Resources\\BinariesNET8\\UniversalGL\\DTAConfig.dll\n  Resources\\BinariesNET8\\UniversalGL\\DTAConfig.pdb\n  Resources\\BinariesNET8\\XNA\\DTAConfig.dll\n  Resources\\BinariesNET8\\XNA\\DTAConfig.pdb\n  ```\n\n## 2.12.10\n\n- 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.\n\n## 2.12.6\n\n- 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.\n\n- 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.\n\n## 2.12.0\n\n- 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.\n\n- 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).\n\n## 2.11.0.0 and earlier\n\n- `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`.\n\n- 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).\n\n- 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.\n\n- 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`.\n\n    - **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).\n\n- Second-stage updater is now automatically copied to `Resources/Binaries/Updater` folder by build scripts.\n\n- 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.\n\n- 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.\n\n- 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.\n\n- 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.\n\n  ```ini\n  [GenericWindow]\n  DrawBorders=false\n  ```\n\n- 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.\n\n- 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:\n  - renaming `PlayerReadyBox*` tags into `PlayerStatusIndicator*` (which now have default values of `0` and `0` instead of `7` and `4` for `X` and `Y` respectively);\n  - 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)):\n    - `statusEmpty.png`;\n    - `statusUnavailable.png`;\n    - `statusAI.png`;\n    - `statusClear.png`;\n    - `statusOk.png`;\n    - `statusInProgress.png`;\n    - `statusWarning.png`;\n    - `statusError.png`;\n\n- 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.\n\n- `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.\n"
  },
  {
    "path": "Docs/NewFeatures.md",
    "content": "# New Features\n\nThis document describes optional, non-breaking changes. While not mandatory, adopting these updates unlocks new client features.\n\nBreaking changes are not covered here; see [Migration.md](Migration.md) instead.\n\n## 2.13.0\n\n- Custom mission support and game mode updates offer several new features. Details will be provided later.\n\n- 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).\n\n- The game icon in the game lobby list can be turned off. See `ShowGameIconInGameList` in [INISystem.md](INISystem.md).\n\n## 2.12.18\n\n- 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.\n\n## 2.12.17\n\n- 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:\n\n```ini\n[Settings]\nCompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe ; comma-separated list of strings to check for DirectDraw compatibility mode issues\n```\n\n- 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:\n\n```ini\n[MultiplayerGameLobby]\n$CCMP100=btnGameLobbySettings:XNAClientButton\n```\n\nThen, add the following `[btnGameLobbySettings]` section:\n\n```ini\n[btnGameLobbySettings]\nText=Lobby Settings\nLocation=0,0\nSize=133,23\nDistanceFromBottomBorder=13\nDistanceFromRightBorder=300\nVisible=false\nEnabled=false\n```\n\n## 2.12.15\n\n- 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:\n\n```iss\n[Registry]\nRoot: HKLM; Subkey: SYSTEM\\CurrentControlSet\\Control\\FileSystem; ValueType: dword; ValueName: LongPathsEnabled; ValueData: 1; MinVersion: 10.0.14393\n```\n\nAlternatively, you can instruct players to enable it by providing the following `.reg` file:\n\n```reg\nWindows Registry Editor Version 5.00\n\n[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\FileSystem]\n\"LongPathsEnabled\"=dword:00000001\n```\n\n## 2.12.13\n\n- 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.\n\n## 2.12.12\n\n- `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`.\n\n## 2.12.10\n\n- `VersionWriter.exe` has been updated with new settings: `ExcludeHiddenAndSystemFiles`, `ApplyTimestampOnVersion`, `NoCopyMode`, and the `[ExcludeDirectories]` section. See [Updater.md](Updater.md).\n\n## 2.12.8\n\n- You can create a `UserDefaults.ini` file in the `Resources` folder to override default settings in the Options window. Example:\n\n```ini\n[Video]\nIntegerScaledClient=True\nBorderlessWindowedClient=False\n\n[Audio]\nClientVolume=0.3\nPlayMainMenuMusic=False\n\n[MultiPlayer]\nNotifyOnUserListChange=False\n```\n\nWe recommend specifying `IntegerScaledClient=True` as the default.\n\n- For `XNAClientColorDropDown` components, the `DisabledItemTexture` key can be used.\n\n## 2.12.7\n\n- 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.\n\n## 2.12.6\n\n- 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.\n\n- Three drawing modes are now available for `XNAClientColorDropDown` components. See `XNAClientColorDropDown` in [INISystem.md](INISystem.md).\n\n## 2.12.5\n\n- 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`.\n\n## 2.12.4\n\n- 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).\n\n## 2.12.2\n\n- The client now supports randomly selecting one loading screen from multiple images. See the `LoadingScreen` section in [INISystem.md](INISystem.md).\n\n## 2.11.7.0\n\n- 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`:\n\n```ini\n[Standard]                          ; any game mode section\n; (...)\nDisallowedPlayerSides=7             ; already exists - disallows sides for all players\nDisallowedHumanPlayerSides=1,2,3    ; new - disallows sides for human players only\nDisallowedComputerPlayerSides=4,5,6 ; new - disallows sides for computer players only\n```\n\n- 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.\n\n## 2.11.2.0\n\n- 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.\n\n## 2.11.1.0\n\n- 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}`.\n\n- 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.\n\n- Documentation has been updated to recommend that Chinese translators use `zh-Hans` or `zh-Hant` as the name of the translation folder.\n\n## 2.11.0.0\n\n- A localization system has been implemented. See [Translation.md](Translation.md).\n\n- The OpenGL variant of the client can now load background music from an `.ogg` file that is placed alongside the corresponding `.wma` file.\n\n- Several network-related definitions can now be customized via `NetworkDefinitions.ini` file in the `Resources` folder. An example is shown below.\n\n```ini\n[Settings]\nCnCNetTunnelListURL=https://cncnet.org/master-list\nCnCNetPlayerCountURL=https://api.cncnet.org/status\nCnCNetMapDBDownloadURL=https://mapdb.cncnet.org\nCnCNetMapDBUploadURL=https://mapdb.cncnet.org/upload\nDisableDiscordIntegration=False\n\n; https://gamesurge.net/servers\n[IRCServers]\n1=irc.gamesurge.net|GameSurge|6667\n2=LAN-Team.DE.EU.GameSurge.net|GameSurge Nuremberg, Germany|6660,6666,6667,6668,6669\n3=Stockholm.SE.EU.GameSurge.net|GameSurge Stockholm, Sweden|6666,6669,7000,8080\n4=NuclearFallout.WA.US.GameSurge.net|GameSurge Seattle, WA|6667,5960\n5=Prothid.NY.US.GameSurge.Net|GameSurge NYC, NY|5960,6660,6666,6667,6668,6669,6697\n6=192.223.27.109|GameSurge IP 192.223.27.109|5960,6660,6666,6667,6668,6669\n7=162.248.94.123|GameSurge IP 162.248.94.123|6667,5960\n8=128.140.107.226|GameSurge IP 128.140.107.226|6660,6666,6667,6668,6669\n9=188.240.145.60|GameSurge IP 188.240.145.60|6660,6666,6667,6668,6669\n```"
  },
  {
    "path": "Docs/Translation.md",
    "content": "# Translation\n\nThe 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.\n\nThe translation system supports the following:\n- translating client's built-in text strings;\n- translating INI-defined text values without modifying the respective INI files themselves;\n- adjusting INI-defined size and position values for client controls per translation;\n- 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);\n- auto-detecting the initial language of the client based on the system's language settings (if provided; happens on first start of the client);\n- configurable set of files to copy to the game directory (for ingame translations);\n- an ability to generate a translation template/stub file for easy translation.\n\n## Translation structure\n\nThe 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.\n\nFor example:\n\n```md\n- Resources\n  - Some Theme Folder\n    * someThemeAsset.png\n    * ...\n  - Translations\n    - ru\n      - Some Theme Folder\n        * Translation.ini\n        * someThemeAsset.png\n        * ...\n      * Translation.ini\n      * someAsset.png\n      * ...\n    - uk\n      * ...\n    - zh-Hans\n      * ...\n    - zh-Hant\n      * ...\n  * someAsset.png\n  * ...\n```\n\n### Folder naming and automatic language detection\n\nThe 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.\n\n> [!NOTE]\n> 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.)\n\nThe 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.\n\n> [!NOTE]\n> 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.\n\n### Translation INI format\n\n```ini\n[General]          ; translation metadata\nName=Some Language ; string, used instead of a system-provided name if set\nAuthor=Someone     ; string\nMapEncoding=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.\n\n[Values]             ; the key-values for translation\nSome:Key=Some Value  ; string, see below for explanation\n```\n\n#### Translation values key format\n\nExamples:\n```ini\nINI:HotkeyCategories:Interface=Интерфейс  ; Interface\nINI:Hotkeys:AllToCheer:Description=Приказать вашей пехоте ликовать.  ; Make all of your infantry units cheer.\nINI:Hotkeys:AllToCheer:UIName=Ликовать  ; Cheer\nINI:Controls:CheaterScreen:lblCheater:Text=Обнаружены изменения!  ; Modifications Detected!\nClient:DTAConfig:ForceUpdate=Принудительное обновление  ; Force Update\nINI:Controls:UpdaterOptionsPanel:btnForceUpdate:Location=320,213\nINI:Controls:UpdaterOptionsPanel:btnForceUpdate:Size=220,23\n```\n\nEach 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).\n- `Client` - the client's built-in text strings.\n  - 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.\n- `INI` - the INI-defined values.\n  - `Controls` - denotes all INI-defined control values.\n    - `[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)\n      - `[control name]` - the name of the control that the value is defined for.\n        - `[attribute name]` - the name of the attribute that is being translated. Currently supported:\n          - `Text`, `Size`, `Width`, `Height`, `Location`, `X`, `Y`, `DistanceFromRightBorder`, `DistanceFromBottomBorder` for every control;\n          - `ToolTip` for controls with tooltip;\n          - `Suggestion` for suggestion text boxes;\n          - `URL`, `UnixURL` for link buttons;\n          - `ItemX` (where X) for setting/game options dropdowns;\n          - `OptionName` for game option dropdowns;\n          - `$X`, `$Y`, `$Width`, `$Height` for INItializable window system.\n  - `Sides` - subcategory for the game's/mod's side names.\n  - `Colors` - subcategory for the game's/mod's color names.\n  - `Themes` - subcategory for the game's/mod's theme names.\n  - `GameModes` - subcategory for the game's/mod's game modes.\n    - `[name]` - uniquely identifies the game mode.\n      - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` is supported.\n  - `Maps` - subcategory for the game's/mod's maps (custom maps are not supported).\n    - `[map path]` - uniquely identifies the map.\n      - `[attribute name]` - the name of the attribute that is being translated. Only `Description` (map name) and `Briefing` are supported.\n  - `Missions` - subcategory for the game's/mod's singleplayer missions.\n    - `[mission section name]` - uniquely identifies the map (taken from `Battle*.ini`).\n      - `[attribute name]` - the name of the attribute that is being translated. Only `Description` (mission name) and `LongDescription` (actual description) are supported.\n  - `CustomComponents` - subcategory for the game's/mod's custom components.\n    - `[custom component INI name]` - uniquely identifies the custom component.\n      - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` is supported.\n  - `UpdateMirrors` - subcategory for the game's/mod's update download mirrors.\n    - `[mirror name]` - uniquely identifies the mirror.\n      - `[attribute name]` - the name of the attribute that is being translated. Only `Name` and `Location` are supported.\n  - `Hotkeys` - subcategory for the game's/mod's hotkeys.\n    - `[INI name]` - uniquely identifies the hotkey.\n      - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` and `Description` are supported.\n  - `HotkeyCategories` - subcategory for the game's/mod's hotkey categories.\n  - `ClientDefinitions` - self explanatory.\n    - `WindowTitle` - self explanatory, only works if set in `ClientDefinitions.ini`\n\n> [!WARNING]\n> 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**.\n\n> [!IMPORTANT]\n> 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.\n\n## Ingame translation setup\n\nThe 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:\n1) the path to the source file relative to currently selected translation directory;\n2) the destination to copy to, relative to the game root folder;\n3) (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.\n\n> [!IMPORTANT]\n> 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.\n>\n> 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.\n>\n> To see links in Windows Explorer, you can install [this extension](https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html).\n\n> [!WARNING]\n> 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.\n\nExample configuration in `ClientDefinitions.ini`:\n```ini\n[Translations]\nGameFileTranslationMix=translation.mix,expandmo98.mix\nGameFile_GDI01=Missions/g0.map,Maps/Missions/g0.map\nGameFile_NOD01=Missions/n0.map,Maps/Missions/n0.map\nGameFile_DLL_SD=Resources/language_800x600.dll, Resources/language_800x600.dll\nGameFile_DLL_HD=Resources/language_1024x720.dll,Resources/language_1024x720.dll\n```\n\nThis will make the `translation.mix` file from current translation folder (say, `Resources/Translations/ru`) copied to game root as `expandmo98.mix` on game start.\n\n> [!WARNING]\n> This feature is needed only for *game* files, not *client* files like INIs, theme assets etc.!\n\n## Suggested translation workflow\n\n0. 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.\n   - 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.\n   - Non-text values (for instance, size and position) are not written to the stub INI, but you can still write them manually if needed.\n1. 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.\n   - 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.\n   - **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\n   - 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.\n     - 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.\n2. 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.\n3. 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).\n4. (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.\n   - 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.\n\nHappy translating!\n\n## Miscellanous\n\n- 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.\n- When translated, original map names still display in a tooltip and can be copied via context menu.\n- Where applicable, both translated and untranslated names are used to search (map and lobby searches).\n"
  },
  {
    "path": "Docs/Updater.md",
    "content": "# Instructions on how to use the updater functionality of the XNA CnCNet client\n\nUpdater-Related Files\n-------------------\n\n### Developer Files\n**These files are needed only by the mod developer and aren't meant to be redistributed to others!** \n- **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).\n- **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.\n\n### Distributable Files\n- **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.\n- **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.\n\nBasic Usage\n-----------\n\n## Quick Guide\n1. Have a web server set up and create a publicly accessible directory from which to download your updates from.\n2. On your client configuration, add URL of the aforementioned directory to list of available download mirrors in `Resources/UpdaterConfig.ini`. \n3. Make changes to files and `VersionConfig.ini`.\n4. Run `VersionWriter.exe`.\n5. Upload the contents of the `VersionWriter_CopiedFiles` and update server scripts to the aforementioned directory on the web server.\n\n## Detailed Instructions\nTo 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.\n\nTo 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.\n\nRefer below for a more comprehensive explanation of both version writer's and updater's features & configuration files.\n\nFeatures\n-------\n\n### Version File Writer\nVersion 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.\n\nThe example `VersionConfig.ini` included with the version file writer in client repository contains comments explaining most of the functionality and features.\n\n`VersionWriter.exe` accepts command-line arguments that start with `/` or `-` as switches. Following switches are accepted:\n- `-LOG`: Generates log file in the program directory.\n- `-QUIET`: Does not generate console output.\n- `-SUPRESSINPUTS`: Does not ask for user input to confirm actions.\n\n 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.\n\n#### Options\nThese are set under `[Options]` in `VersionConfig.ini`.\n- `EnableExtendedUpdaterFeatures`: If set, enables additional updater features such as compressed archives, updater version and manual download URL.\n- `RecursiveDirectorySearch`: If set, will go through every subdirectory recursively for directories given in `[Include]`.\n- `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.\n- `CopyArchivedOriginalFiles`: If set, original versions of archived files will also be copied to copied files directory.\n- `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`.\n- `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.\n- `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.\n\n#### Updater Version & Manual Download URL\nSetting `[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.\n\nAdditionally 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.\n\n#### Compressed Archives\nThe 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.\n\n#### Custom Components\nCustom 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.\n\n- 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.\n\n### Updater Configuration\nThe example `Resources/UpdaterConfig.ini` included with client files contains comments explaining most of the functionality and features.\n\nThe 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.\n\n#### Download Mirrors\nList 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.\n\nUpdater and Updater & Component options in client options will be unavailable if no download mirrors are found.\n\n#### Custom Components\nList 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.\n\nDownload 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.\n\nDownload path archive file extension disable flag is a boolean value (yes/no, true/false), is optional and defaults to false.\n\nCustom components and the Components tab in client options will be unavailable if no custom component info is found.\n"
  },
  {
    "path": "GitVersion.yml",
    "content": "# Configuration docs: https://gitversion.net/docs/reference/configuration\n#\n# This configuration will allow the gitversion msbuild task to auto version our executable. It uses existing tags\n# to determine the next version by auto incrementing the Minor, Patch, or Tag version. The Tag version is calculated\n# by counting the number of commits since the last tag.\n# Patch version is incremented by 1 when a build is created from the develop or master branch.\n#\n# Examples:\n#\n# Latest tag is \"2.8.0\" - \n# The next commit on develop branch will start version 2.8.1-beta.1.\n# That increments the Patch by 1. That also increments the beta by 1 for the 1 additional commit.\n# Another commit creates 2.8.1-beta.2 and so on.\n#\n# Latest tag is \"2.8.0-beta.1\" - \n# The next commit on develop branch is \"2.8.0-beta.2\". Patch version is NOT incremented, because the \n# base tag of \"2.8.0-beta.1\" is also a beta.\n# \n# Latest tag is \"2.8.0\" -\n# A new commit is added directly to master. The new version is \"2.8.1-rc.1\".\n# That increments the Patch, because it is treated as a \"hotfix\" directly on master.\n#\n# Versioning Modes Quick View:\n# \n# Continuous Delivery: The default versioning mode. In this mode, GitVersion calculates the next version and will use that until that is released.\n# Continuous Deployment: Sometimes you just want the version to keep changing and deploy continuously.\n# In this case, Continuous Deployment is a good mode to operate GitVersion by.\n\ntag-prefix: 'v'\n\nbranches:\n  master:\n    regex: ^master$\n    \n    mode: ContinuousDelivery\n    increment: Patch\n    is-mainline: true\n    source-branches: [ 'develop' ]\n    tag: rc\n  \n  develop:\n    regex: ^develop$\n    increment: Patch\n    is-mainline: false\n    mode: ContinuousDeployment\n    tag: beta\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "NuGet.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear/>\n    <add key=\"NuGet official package source\" value=\"https://api.nuget.org/v3/index.json\" />\n    <add key=\"Local file package source\" value=\"References\" />\n  </packageSources>\n</configuration>"
  },
  {
    "path": "README.md",
    "content": "# CnCNet Client\n\nThe 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!\n\nYou can find the [dedicated project development chat](https://discord.gg/M5gGdBYG5m) at C&C Mod Haven Discord server.\n\n## Targets\n\nThe primary targets of the client project are\n* [Dawn of the Tiberium Age](https://www.moddb.com/mods/the-dawn-of-the-tiberium-age)\n* [Twisted Insurrection](https://www.moddb.com/mods/twisted-insurrection)\n* [Mental Omega](https://www.moddb.com/mods/mental-omega)\n* [CnCNet Yuri's Revenge](https://cncnet.org/yuris-revenge)\n\nHowever, 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).\n\n## Development requirements\n\nThe client supports 2 runtimes: .NET 4.8 and .NET 8.0.\n* Both runtimes have 3 rendering engines: Windows DirectX11, Windows OpenGL and Windows XNA.\n* .NET 8.0 in addition has a cross-platform Universal OpenGL engine.\n* The DirectX11 and OpenGL engines rely on MonoGame.\n* The XNA engine relies on Microsoft's XNA Framework 4.0 Refresh.\n\nTo 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:\n```shell\ngit submodule update --init --recursive\n```\n\nBuilding 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.\nTo debug WindowsXNA builds the .NET SDK 10.0 x86 is additionally required.\nWhen using the included build scripts, [PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows) is required.\n\n## Building and debugging\n\n* 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.\n\n* 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).\n\n> [!IMPORTANT]\n> 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**.\n\n### Advanced notes on building and debugging\n\n* 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.\n\n* 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.\n\n* 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.\n\n* 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. \n\nRefer to [Docs/Build.md](/Docs/Build.md) for more information about building the client.\n\n## End-user requirements\n\n* 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.\n\n* Other OS: Use the Universal OpenGL engine.\n\n### Windows .NET 4.8 requirements:\n\n* The [.NET Framework 4.8 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet-framework/thank-you/net48-web-installer)\n\n(Optional) The XNA engine requires:\n* [Microsoft XNA Framework Redistributable 4.0 Refresh](https://www.microsoft.com/en-us/download/details.aspx?id=27598).\n\n### Linux requirements:\n\n* The [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=linux) for your specific platform.\n\n### macOS requirements:\n\n* The [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=macos) for your specific platform.\n\n### Windows .NET 8.0 requirements:\n\n<details>\n  <summary>Windows .NET 8.0 requirements</summary>\n\n* The [.NET 8.0 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=windows) for your specific platform.\n\n(Optional) The XNA engine requires:\n* [Microsoft XNA Framework Redistributable 4.0 Refresh](https://www.microsoft.com/en-us/download/details.aspx?id=27598).\n* [.NET 8.0 Desktop Runtime x86](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.0-windows-x86-installer).\n\nWindows 7 SP1 and Windows 8.x additionally require:\n* 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.\n\nWindows 7 SP1 additionally requires:\n* KB3063858 [64-bit](https://www.microsoft.com/download/details.aspx?id=47442) / [32-bit](https://www.microsoft.com/download/details.aspx?id=47409).\n</details>\n\n## Client launcher\n\nThis 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.\nSee [xna-cncnet-client-launcher](https://github.com/CnCNet/xna-cncnet-client-launcher).\n\n## Branches\n\nCurrently 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.\n\n## Screenshots\n\n![Screenshot](cncnetchatlobby.png?raw=true \"CnCNet IRC Chat Lobby\")\n![Screenshot](cncnetgamelobby.png?raw=true \"CnCNet Game Lobby\")\n\n## License\n\nCnCNet Client\nCopyright (C) 2013-2026 CnCNet, Rampastring\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n### Additional permission under GNU GPL version 3 section 7\n\nIf 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.\n\n## Sponsored by\n\n<a href=\"https://www.digitalocean.com/?refcode=337544e2ec7b&utm_campaign=Referral_Invite&utm_medium=opensource&utm_source=CnCNet\" title=\"Powered by Digital Ocean\" target=\"_blank\">\n    <img src=\"https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg\" width=\"201px\" alt=\"Powered By Digital Ocean\" />\n</a>\n"
  },
  {
    "path": "References/.gitkeep",
    "content": ""
  },
  {
    "path": "Scripts/Build.bat",
    "content": "@echo off\nwhere pwsh > nul 2> nul\nif %errorlevel% equ 0 (\n  pwsh -ExecutionPolicy Bypass -File build.ps1\n) else (\n  echo \"Please Install PowerShell.\"\n  echo \"https://aka.ms/pscore6\"\n)\npause\n"
  },
  {
    "path": "Scripts/ClearBinAndObjDirs.bat",
    "content": "@echo off\n\ncd /d %~dp0\ncd ..\n\nfor /f \"tokens=*\" %%f in ('dir \".\\\" /a:d /b') do (\n\trmdir /q /s \"%%f\\bin\" > nul 2> nul\n\trmdir /q /s \"%%f\\obj\" > nul 2> nul\n)\n"
  },
  {
    "path": "Scripts/Get-CommonAssemblyList.ps1",
    "content": "#!/usr/bin/env pwsh\n#Requires -Version 7.2\n\n# /// WARNING /// WARNING /// WARNING ///\n#\n# DO NOT CHANGE OUTPUT FOR THIS SCRIPT! \n#\n# /// WARNING /// WARNING /// WARNING ///\n\n# This script generates a list of generic assemblies by calculating the contents generated by the `. \\build.ps1 -NoMove` command.\n\n[CmdletBinding()]\nparam (\n  [Parameter()]\n  [switch]\n  $Net8\n)\n\n[string]$Script:RepoRoot = Split-Path $PSScriptRoot\n[string]$Script:CompiledRoot = Join-Path $RepoRoot 'Compiled'\n[string]$Script:GamePath = $CompiledRoot\n[string]$Script:Resources = Join-Path $GamePath 'Resources'\n[string]$Script:Binaries = Join-Path $Resources 'Binaries'\nif ($Net8) {\n  $Script:Binaries = Join-Path $Resources 'BinariesNET8'\n}\n\n[System.Collections.Generic.List[string]]$Script:Engines = @('OpenGL', 'Windows', 'XNA')\nif ($Net8) {\n  $Script:Engines.Add('UniversalGL')\n}\n\n[hashtable]$Script:FileHashTable = @{}\n$Script:Engines | ForEach-Object {\n  [string]$Private:Engine = $PSItem\n  [string]$Private:PlatformFolder = Join-Path $Binaries $Private:Engine\n\n  Get-ChildItem $Private:PlatformFolder | Where-Object {\n    $PSItem -is [System.IO.FileInfo]\n  } | ForEach-Object {\n    if (!$Script:FileHashTable.ContainsKey($PSItem.Name)) {\n      $Script:FileHashTable[$PSItem.Name] = [hashtable]@{}\n    }\n\n    $Script:FileHashTable[$PSItem.Name][$Engine] = Get-FileHash $PSItem\n  }\n}\n\n$Script:FileList = $Script:FileHashTable.Keys | Where-Object {\n  $Private:Key = $PSItem\n  if ($Script:FileHashTable[$Private:Key].Count -ne $Script:Engines.Count) {\n    return $false\n  }\n  [string]$hash = $null\n  foreach ($item in $Script:FileHashTable[$Private:Key].Values) {\n    if ([string]::IsNullOrEmpty($hash)) {\n      $hash = $item.Hash\n    }\n    elseif ($hash -ne $item.Hash) {\n      return $false\n    }\n  }\n  return $true\n}\n\n$Script:FileList | Sort-Object -Unique"
  },
  {
    "path": "Scripts/README.md",
    "content": "# README for Build Scripts\n\n> [!NOTE]\n> Before running any scripts in this folder, please close Visual Studio.\n\n## Build the client\n\nDouble-click the following script file: `Build.bat`.\n\n## Update the common assembly list\n\nYou should do this if you have introduced any new NuGet dependencies.\n\n1. Launch Powershell (`pwsh`, not `PowerShell`) and switch to this folder. \n\n2. `.\\build.ps1 -NoMove`\n\n3. `.\\Get-CommonAssemblyList.ps1 -Net8 > ..\\CommonAssemblies.txt`\n\n4. `.\\Get-CommonAssemblyList.ps1 > ..\\CommonAssembliesNetFx.txt`\n\n5. Carefully check the changes with Git diff:\n- If you have introduce new NuGet dependencies, check if they have appeared in the list.\n    - If they do show in the list, it's expected.\n    - 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.\n- If there are other libraries get **removed** from this list, don't just commit the changes. Does this library exist in the `Compiled` folder?\n    - If so, we can **resume** this line instead of removing it. \n    - If not, think carefully if we should keep this item, depending on whether these libraries should differ among DX/GL/XNA builds.\n        - 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.\n- 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.\n    - 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.\n    - 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.\n\n6. Delete the `Compiled` folder since it is produced with `-NoMove` parameter. We should absolutely **not** distribute these files.\n"
  },
  {
    "path": "Scripts/build.ps1",
    "content": "#!/usr/bin/env pwsh\n#Requires -Version 7.2\n\n#####################################################################\n#\n# Note: \n#    Be careful to synchronize changes to `Directory.Build.targets`\n#    when making changes to paths.\n#\n#####################################################################\n\n<#\n.SYNOPSIS\n  Builds XNA CnCNet Client using specified parameters.\n.DESCRIPTION\n  You can use this script to make publish packages for your game.\n.PARAMETER IsDebug\n  Build projects in debug mode.\n.PARAMETER Log\n  Detail log.\n.PARAMETER NoClean\n  Do not clean Compiled folder.\n.PARAMETER NoMove\n  Do not make folder structure.\n.EXAMPLE\n  build.ps1\n  Build.\n.EXAMPLE\n  build.ps1 -IsDebug\n  Build on debug mode.\n#>\nparam(\n  [Parameter()]\n  [switch]\n  $IsDebug,\n  [Parameter()]\n  [switch]\n  $Log,\n  [Parameter()]\n  [switch]\n  $NoClean,\n  [Parameter()]\n  [switch]\n  $NoMove\n)\n\n$Script:ConfigurationSuffix = 'Release'\nif ($IsDebug) {\n  $Script:ConfigurationSuffix = 'Debug'\n}\n\n$Script:RepoRoot = Split-Path $PSScriptRoot\n$Script:ProjectPath = Join-Path $RepoRoot 'DXMainClient' 'DXMainClient.csproj'\n$Script:CompiledRoot = Join-Path $RepoRoot 'Compiled'\n$Script:EngineSubFolderMap = @{\n  'UniversalGL' = 'UniversalGL'\n  'WindowsDX'   = 'Windows'\n  'WindowsGL'   = 'OpenGL'\n  'WindowsXNA'  = 'XNA'\n}\n$Script:FrameworkBinariesFolderMap = @{\n  'net48'          = 'Binaries'\n  'net8.0'         = 'BinariesNET8'\n  'net8.0-windows' = 'BinariesNET8'\n}\n\nif (!$NoClean -AND (Test-Path $Script:CompiledRoot)) {\n  Remove-Item -Recurse -Force -LiteralPath $Script:CompiledRoot\n}\n\nif ($null -EQ $IsWindows -AND 'Desktop' -EQ $PSEdition) {\n  $Script:IsWindows = $true\n}\n\nfunction Script:Invoke-BuildProject {\n  [CmdletBinding(DefaultParameterSetName = 'ByGame')]\n  param (\n    [Parameter(Mandatory, ParameterSetName = 'Detail', Position = 0)]\n    [string]\n    $Engine,\n    [Parameter(Mandatory, ParameterSetName = 'Detail')]\n    [string]\n    $Framework\n  )\n  \n  process {\n    if ($Engine) {\n      $Output = Join-Path $CompiledRoot 'Resources' ($FrameworkBinariesFolderMap[$Framework]) ($EngineSubFolderMap[$Engine])\n\n      $Private:ArgumentList = [System.Collections.Generic.List[string]]::new(11)\n      $Private:ArgumentList.Add('publish')\n      $Private:ArgumentList.Add(\"$ProjectPath\")\n      $Private:ArgumentList.Add('--graph')\n      $Private:ArgumentList.Add(\"--configuration:${Engine}$Script:ConfigurationSuffix\")\n      $Private:ArgumentList.Add(\"--framework:$Framework\")\n      $Private:ArgumentList.Add(\"--output:$Output\")\n      $Private:ArgumentList.Add('-property:SatelliteResourceLanguages=en')\n      if ($Log) {\n        $Private:ArgumentList.Add('-verbosity:diagnostic')\n      }\n      if ($NoMove) {\n        $Private:ArgumentList.Add('-property:NoMove=true')\n      }\n      # $Private:ArgumentList.Add(\"-property:AssemblyVersion=$AssemblySemVer\")\n      # $Private:ArgumentList.Add(\"-property:FileVersion=$AssemblySemFileVer\")\n      # $Private:ArgumentList.Add(\"-property:InformationalVersion=$InformationalVersion\")\n  \n      # if ($Engine -eq 'WindowsXNA') {\n      #   $Private:ArgumentList.Add('--arch=x86')\n      # }\n  \n      & 'dotnet' $Private:ArgumentList\n      if ($LASTEXITCODE) {\n        throw \"Build failed for ${Engine}$Script:ConfigurationSuffix $Framework (exit code $LASTEXITCODE)\"\n      }\n    }\n    else {\n      Invoke-BuildProject -Engine 'UniversalGL' -Framework 'net8.0'\n      if ($IsWindows) {\n        @('WindowsDX', 'WindowsGL', 'WindowsXNA') | ForEach-Object {\n          $Private:Engine = $PSItem\n  \n          @('net48', 'net8.0-windows') | ForEach-Object {\n            $Private:Framework = $PSItem\n  \n            Invoke-BuildProject -Engine $Private:Engine -Framework $Private:Framework\n          }\n        }\n      }\n    }\n  }\n}\n\nScript:Invoke-BuildProject\n"
  },
  {
    "path": "SecondStageUpdater/Program.cs",
    "content": "﻿// Copyright 2022-2025 CnCNet\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY, without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\n#pragma warning disable IDE0057 // Use range operator\n\nnamespace SecondStageUpdater;\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Threading;\n\nusing Rampastring.Tools;\n\ninternal sealed class Program\n{\n    private const int MutexTimeoutInSeconds = 30;\n\n    private static ConsoleColor defaultColor;\n    private static bool hasHandle;\n    private static Mutex clientMutex;\n\n    // e.g. args = [\"clientogl.dll\", \"\\\"C:\\\\Game\\\\\\\"\"];\n    private static void Main(string[] args)\n    {\n        defaultColor = Console.ForegroundColor;\n\n        try\n        {\n            Write(\"CnCNet Client Second-Stage Updater\", true, ConsoleColor.Green);\n            Write(string.Empty);\n\n            if (args.Length < 2 || string.IsNullOrEmpty(args[0]) || string.IsNullOrEmpty(args[1]))\n            {\n                Write(\"Invalid arguments given!\", true, ConsoleColor.Red);\n                Write(\"Usage: <client_executable_name> <base_directory>\");\n                Write(string.Empty);\n                Exit(false);\n            }\n\n            DirectoryInfo baseDirectory = SafePath.GetDirectory(args[1].Replace(\"\\\"\", null));\n\n            if (!baseDirectory.Exists)\n            {\n                Write(\"Base directory does not exist!\", true, ConsoleColor.Red);\n                Write(baseDirectory.FullName);\n                Write(string.Empty);\n                Exit(false);\n            }\n            else\n            {\n                string clientExecutable = args[0];\n                DirectoryInfo resourceDirectory = SafePath.GetDirectory(baseDirectory.FullName, \"Resources\");\n                FileInfo logFile = SafePath.GetFile(SafePath.CombineFilePath(baseDirectory.FullName, \"Client\", \"SecondStageUpdater.log\"));\n\n                if (logFile.Exists)\n                    logFile.Delete();\n\n                Logger.Initialize(logFile.DirectoryName, logFile.Name);\n                Logger.WriteLogFile = true;\n                Logger.WriteToConsole = false;\n                Logger.Log(\"CnCNet Client Second-Stage Updater\");\n                Logger.Log(\"Version: \" + GitVersionInformation.AssemblySemVer);\n                Write(\"Base directory: \" + baseDirectory.FullName);\n                Write($\"Waiting for the client ({clientExecutable}) to exit..\");\n\n                // note: the GUID should be consistent with the one in xna-cncnet-client/DXMainClient/Program.cs\n                string clientMutexId = FormattableString.Invariant($\"Global{Guid.Parse(\"1CC9F8E7-9F69-4BBC-B045-E734204027A9\")}\");\n\n                clientMutex = new(false, clientMutexId, out _);\n\n                try\n                {\n                    hasHandle = clientMutex.WaitOne(TimeSpan.FromSeconds(MutexTimeoutInSeconds), false);\n                }\n                catch (AbandonedMutexException)\n                {\n                    hasHandle = true;\n                }\n\n                if (!hasHandle)\n                {\n                    Write($\"Timeout while waiting for the client ({clientExecutable}) to exit!\", true, ConsoleColor.Red);\n                    Exit(false);\n                }\n\n                // This is occasionally necessary to prevent DLLs from being locked at the time that this update is attempting to overwrite them\n                Thread.Sleep(1000);\n\n                DirectoryInfo updaterDirectory = SafePath.GetDirectory(baseDirectory.FullName, \"Updater\");\n\n                if (!updaterDirectory.Exists)\n                {\n                    Write($\"{updaterDirectory.Name} directory does not exist!\", true, ConsoleColor.Red);\n                    Exit(false);\n                }\n\n                Write(\"Updating files.\", true, ConsoleColor.Green);\n\n                IEnumerable<FileInfo> files = updaterDirectory.EnumerateFiles(\"*\", SearchOption.AllDirectories);\n                FileInfo executableFile = SafePath.GetFile(Assembly.GetExecutingAssembly().Location);\n                FileInfo relativeExecutableFile = SafePath.GetFile(executableFile.FullName.Substring(baseDirectory.FullName.Length));\n\n                const string versionFileName = \"version\";\n\n                Write($\"{nameof(SecondStageUpdater)}: {relativeExecutableFile}\");\n\n                AssemblyName[] assemblies = Assembly.LoadFrom(executableFile.FullName).GetReferencedAssemblies();\n\n                foreach (FileInfo fileInfo in files)\n                {\n                    string relativeFileName = fileInfo.FullName.Substring(updaterDirectory.FullName.Length);\n                    string fileExtension = fileInfo.Extension;\n                    string relativeFileNameWithoutExtension = relativeFileName.Substring(0, relativeFileName.Length - fileExtension.Length);\n\n                    string relativeExecutableFileFullName = relativeExecutableFile.FullName;\n                    string relativeExecutableFileExtension = relativeExecutableFile.Extension;\n                    string relativeExecutableFileFullNameWithoutExtension = relativeExecutableFileFullName.Substring(0, relativeExecutableFileFullName.Length - relativeExecutableFileExtension.Length);\n\n                    if (relativeFileNameWithoutExtension.Equals(relativeExecutableFileFullNameWithoutExtension, StringComparison.OrdinalIgnoreCase)\n                        || relativeFileNameWithoutExtension.Equals(SafePath.CombineFilePath(\"Resources\", Path.GetFileNameWithoutExtension(relativeExecutableFile.Name)), StringComparison.OrdinalIgnoreCase))\n                    {\n                        Write($\"Skipping {nameof(SecondStageUpdater)} file {relativeFileName}\");\n                    }\n                    else if (assemblies.Any(q => relativeFileNameWithoutExtension.Equals(q.Name, StringComparison.OrdinalIgnoreCase))\n                        || assemblies.Any(q => relativeFileNameWithoutExtension.Equals(SafePath.CombineFilePath(\"Resources\", q.Name), StringComparison.OrdinalIgnoreCase)))\n                    {\n                        Write($\"Skipping {nameof(SecondStageUpdater)} dependency {relativeFileName}\");\n                    }\n                    else if (relativeFileName.Equals(versionFileName, StringComparison.OrdinalIgnoreCase))\n                    {\n                        Write($\"Skipping {relativeFileName}\");\n                    }\n                    else\n                    {\n                        try\n                        {\n                            FileInfo copiedFile = SafePath.GetFile(baseDirectory.FullName, relativeFileName);\n\n                            Write($\"Updating {relativeFileName}\");\n\n                            // If the file is read-only, we need to remove the read-only attribute before copying it\n                            if (copiedFile.Exists && copiedFile.IsReadOnly)\n                            {\n                                copiedFile.IsReadOnly = false;\n                                fileInfo.CopyTo(copiedFile.FullName, true);\n                                copiedFile.IsReadOnly = true;\n                            }\n                            else\n                            {\n                                fileInfo.CopyTo(copiedFile.FullName, true);\n                            }\n                        }\n                        catch (Exception ex)\n                        {\n                            Write($\"Updating file failed! Returned error message: {ex}\", true, ConsoleColor.Yellow);\n                            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.\");\n                            Exit(false);\n                        }\n                    }\n                }\n\n                FileInfo versionFile = SafePath.GetFile(updaterDirectory.FullName, versionFileName);\n\n                if (versionFile.Exists)\n                {\n                    FileInfo destinationFile = SafePath.GetFile(baseDirectory.FullName, versionFile.Name);\n                    FileInfo relativeFileInfo = SafePath.GetFile(destinationFile.FullName.Substring(baseDirectory.FullName.Length));\n\n                    Write($\"Updating {relativeFileInfo}\");\n                    versionFile.CopyTo(destinationFile.FullName, true);\n                }\n\n                Write(\"Files successfully updated. Starting launcher..\", true, ConsoleColor.Green);\n                string launcherExe = string.Empty;\n\n                try\n                {\n                    Write(\"Checking ClientDefinitions.ini for launcher executable filename.\");\n\n                    string[] lines = File.ReadAllLines(SafePath.CombineFilePath(resourceDirectory.FullName, \"ClientDefinitions.ini\"));\n                    string launcherPropertyName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? \"LauncherExe\" : \"UnixLauncherExe\";\n                    string line = lines.Single(q => q.Trim().StartsWith(launcherPropertyName, StringComparison.OrdinalIgnoreCase) && q.Contains('='));\n                    int commentStart = line.IndexOf(\";\", StringComparison.OrdinalIgnoreCase);\n\n                    if (commentStart >= 0)\n                        line = line.Substring(0, commentStart);\n\n                    launcherExe = line.Split('=')[1].Trim();\n                }\n                catch (Exception ex)\n                {\n                    Write($\"Failed to read ClientDefinitions.ini: {ex}\", true, ConsoleColor.Yellow);\n                }\n\n                FileInfo architectureLauncherExeFile = SafePath.GetFile(resourceDirectory.FullName, \"Launcher\", FormattableString.Invariant($\"{Path.GetFileNameWithoutExtension(launcherExe)}-{RuntimeInformation.OSArchitecture}{Path.GetExtension(launcherExe)}\"));\n                FileInfo launcherExeFile = SafePath.GetFile(baseDirectory.FullName, launcherExe);\n\n                if (architectureLauncherExeFile.Exists)\n                {\n                    architectureLauncherExeFile.CopyTo(launcherExeFile.FullName, true);\n                    launcherExeFile.Refresh();\n                }\n\n                if (launcherExeFile.Exists)\n                {\n                    Write(\"Launcher executable found: \" + launcherExe, true, ConsoleColor.Green);\n\n                    using var _ = Process.Start(new ProcessStartInfo\n                    {\n                        FileName = launcherExeFile.FullName\n                    });\n                }\n                else\n                {\n                    Write(\"No suitable launcher executable found! Client will not automatically start after updater closes.\", true, ConsoleColor.Yellow);\n                    Exit(false);\n                }\n            }\n\n            Exit(true);\n        }\n        catch (Exception ex)\n        {\n            Write(\"An error occurred during the Launcher Updater's operation.\", true, ConsoleColor.Red);\n            Write($\"Returned error was: {ex}\");\n            Write(string.Empty);\n            Write(\"If you were updating a game, please try again. If the problem continues, contact the staff for support.\");\n            Exit(false);\n        }\n    }\n\n    private static void Exit(bool success)\n    {\n        if (hasHandle)\n        {\n            clientMutex.ReleaseMutex();\n            clientMutex.Dispose();\n        }\n\n        if (!success)\n        {\n            Write(\"Press any key to exit.\");\n            Console.ReadKey();\n            Environment.Exit(1);\n        }\n    }\n\n    private static void Write(string text, bool logToFile = true, ConsoleColor? color = null)\n    {\n        Console.ForegroundColor = color ?? defaultColor;\n        Console.WriteLine(text);\n\n        if (logToFile)\n            Logger.Log(text);\n    }\n}"
  },
  {
    "path": "SecondStageUpdater/SecondStageUpdater.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <UseAppHost>false</UseAppHost>\n  </PropertyGroup>\n  <PropertyGroup>\n    <Title>CnCNet.SecondStageUpdater</Title>\n    <Description>CnCNet Client Second-Stage Updater</Description>\n    <Product>CnCNet.SecondStageUpdater</Product>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Rampastring.XNAUI\\Rampastring.Tools\\Rampastring.Tools.csproj\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "TranslationNotifierGenerator/StringExtensions.cs",
    "content": "﻿using Microsoft.CodeAnalysis.CSharp;\n\nnamespace TranslationNotifierGenerator\n{\n    public static class StringExtensions\n    {\n        public static string ToLiteral(this string input)\n        {\n            // https://stackoverflow.com/a/55798623\n            return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(input)).ToFullString();\n        }\n    }\n}"
  },
  {
    "path": "TranslationNotifierGenerator/TranslationNotifierGenerator.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\n\nnamespace TranslationNotifierGenerator\n{\n    /// <summary>\n    /// Generates a <c>TranslationNotifier</c> class that allows to notify the translation system\n    /// about all hardcoded missing translation strings by calling <c>TranslationNotifier.Register()</c>.\n    /// </summary>\n    /// <remarks>\n    /// It is required to make <c>RootNamespace</c> project property visible to the compiler via <c>CompilerVisibleProperty</c>\n    /// (already handled in <c>Directory.Build.props</c>). This is required to generate the correct namespace for the generated class.\n    /// </remarks>\n    [Generator]\n    public class TranslationNotifierGenerator : ISourceGenerator\n    {\n        // Change those if you change the method names\n        public const string LocalizeMethodContainingNamespace = \"ClientCore.Extensions\";\n        public const string LocalizeMethodName = \"L10N\";\n\n        public void Execute(GeneratorExecutionContext context)\n        {\n            // uncomment to debug the generator\n            //Debug.WriteLine($\"Executing {nameof(TranslationNotifierGenerator)}...\");\n\n            var compilation = context.Compilation;\n\n            _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($\"build_property.RootNamespace\", out string namespaceName);\n            if (!namespaceName.Split(new char[] { '.' }).All(name => SyntaxFacts.IsValidIdentifier(name)))\n                throw new Exception(\"The namespace can not contain invalid characters.\");\n\n            Dictionary<string, string> translations = new();\n            foreach (var tree in compilation.SyntaxTrees)\n            {\n                context.CancellationToken.ThrowIfCancellationRequested();\n                // https://stackoverflow.com/questions/43679690/with-roslyn-find-calling-method-from-string-literal-parameter\n                var memberAccessSyntaxes = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>();\n                foreach (var memberAccessSyntax in memberAccessSyntaxes)\n                {\n                    context.CancellationToken.ThrowIfCancellationRequested();\n                    if (memberAccessSyntax == null\n                        || !memberAccessSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression)\n                        || memberAccessSyntax.Name.ToString() != LocalizeMethodName)\n                    {\n                        continue;\n                    }\n\n                    if (memberAccessSyntax.Parent is not InvocationExpressionSyntax l10nSyntax\n                        || l10nSyntax.ArgumentList.Arguments.Count == 0\n                        || !l10nSyntax.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression))\n                    {\n                        continue;\n                    }\n\n                    // https://stackoverflow.com/questions/35670115/how-to-use-roslyn-to-get-compile-time-constant-value\n                    var semanticModel = compilation.GetSemanticModel(l10nSyntax.SyntaxTree);\n\n                    // Get the key and the value.\n                    var keyNameSyntax = l10nSyntax.ArgumentList.Arguments[0];\n                    string keyName = semanticModel.GetConstantValue(keyNameSyntax.Expression).Value?.ToString();\n                    Debug.Assert(keyName is null == !keyNameSyntax.Expression.IsKind(SyntaxKind.StringLiteralExpression));\n                    bool keyNameIsPotentiallyForIni = keyName is null || keyName.StartsWith(\"INI:\");\n\n                    var valueTextSyntax = l10nSyntax.Expression as MemberAccessExpressionSyntax;\n                    string valueText = semanticModel.GetConstantValue(valueTextSyntax.Expression).Value?.ToString();\n\n                    if (keyNameIsPotentiallyForIni)\n                    {\n                        if (valueText is not null)\n                        {\n                            context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(\n                                \"CNCNET0001\", \"Literal INI translation value\",\n                                \"The value of an INI translation should not be a compile-time string.\",\n                                \"CNCNET\", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation()));\n                        }\n\n                        continue;\n                    }\n\n                    if (keyName is not null && valueText is null)\n                    {\n                        context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(\n                            \"CNCNET0002\", \"Non-literal translation value\",\n                            $\"Failed to get the value of key {keyName} as a compile-time string.\",\n                            \"CNCNET\", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation()));\n                        continue;\n                    }\n\n                    // Check for duplicates.\n                    if (translations.ContainsKey(keyName))\n                    {\n                        if (valueText != translations[keyName])\n                        {\n                            context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(\n                                \"CNCNET0003\", \"Conflict translation items\",\n                                $\"Key {keyName} is defined more than once and the values are not the same.\",\n                                \"CNCNET\", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation()));\n                        }\n\n                        continue;\n                    }\n\n                    // Avoid trimmable strings\n                    if (valueText.Trim() != valueText)\n                    {\n                        context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(\n                            \"CNCNET0004\", \"Trimmable translation value\",\n                            $\"The value of key {keyName} should not have leading or trailing whitespace.\",\n                            \"CNCNET\", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation()));\n                    }\n\n                    translations.Add(keyName, valueText);\n                }\n            }\n\n            context.CancellationToken.ThrowIfCancellationRequested();\n            var sb = new StringBuilder();\n            _ = sb.AppendLine(@\"\nusing System.Collections.Generic;\");\n            _ = sb.AppendLine($\"using {LocalizeMethodContainingNamespace};\");\n\n            _ = sb.AppendLine($\"namespace {namespaceName}.Generated;\");\n            _ = sb.AppendLine(@\"\npublic class TranslationNotifier\n{\n    public static void Register()\n    {\");\n            foreach (var kv in translations)\n                _ = sb.AppendLine($\"        {kv.Value.ToLiteral()}.{LocalizeMethodName}({kv.Key.ToLiteral()});\");\n\n            _ = sb.AppendLine(@\"    }\n}\n\");\n\n            context.CancellationToken.ThrowIfCancellationRequested();\n            context.AddSource($\"TranslationNotifier.Generated.cs\", sb.ToString());\n        }\n\n        public void Initialize(GeneratorInitializationContext context)\n        {\n            // uncomment to debug the generator\n            //if (!Debugger.IsAttached)\n            //    Debugger.Launch();\n\n            //Debug.WriteLine($\"Initalized {nameof(TranslationNotifierGenerator)}...\");\n        }\n    }\n}"
  },
  {
    "path": "TranslationNotifierGenerator/TranslationNotifierGenerator.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>netstandard2.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.CodeAnalysis.Analyzers\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.CodeAnalysis.CSharp.Workspaces\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"rollForward\": \"latestFeature\",\n    \"version\": \"10.0.100\"\n  }\n}"
  }
]