Skip to content

Commit e972bba

Browse files
committed
Merge branch 'develop' of https://github.com/CO2-code/xna-cncnet-client into develop
2 parents 6174571 + 3e0bb14 commit e972bba

40 files changed

Lines changed: 1185 additions & 304 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# GitHub Copilot coding agent setup instructions
2+
3+
This section only applies to the GitHub Copilot coding agent, running in a Linux runner from the GitHub Action environment. It does not apply to other environments, such as local development.
4+
5+
The GitHub Actions workflow `.github/workflows/copilot-setup-steps.yml` runs the setup steps mentioned in this file automatically. The commands below are the manual equivalent and **should only be run if you encounter a build failure** — for example, if GitVersion cannot determine the version, if submodules are missing, or if NuGet restore fails.
6+
7+
## Step 1 — Initialize git submodules
8+
9+
`Rampastring.XNAUI` (and its nested submodule `Rampastring.Tools`) may not be pre-initialized. Missing them causes compile errors about unknown `Rampastring.*` types.
10+
11+
```shell
12+
git submodule update --init --recursive
13+
```
14+
15+
## Step 2 — Unshallow the clone and fetch `develop`
16+
17+
The build system uses **GitVersion.MsBuild** to compute version numbers at compile time. It requires two things:
18+
19+
- A full (non-shallow) commit history.
20+
- 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`.
21+
22+
Run all three commands unconditionally:
23+
24+
- `--unshallow` is a no-op on an already-full clone (`|| true` prevents it from aborting).
25+
- `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.
26+
- The final fetch brings `refs/remotes/origin/develop` into the local ref store through that glob refspec so GitVersion can find it.
27+
28+
The same fix must be applied to every submodule recursively: `Rampastring.XNAUI` and its nested `Rampastring.Tools` submodule also carry `GitVersion.MsBuild` and are subject to the same crash when checked out with a narrow single-branch refspec.
29+
30+
```shell
31+
git fetch --unshallow origin || true
32+
git remote set-branches origin '*'
33+
git fetch origin develop
34+
git submodule foreach --recursive \
35+
'git fetch --unshallow origin || true; git remote set-branches origin "*"; git fetch origin'
36+
```
37+
38+
## Step 3 — Restore NuGet packages
39+
40+
Run restore from the **repo root** so that the solution file (`DXClient.slnx`) is used. This ensures all projects — including `SecondStageUpdater`, which the build pulls in transitively — are restored. Always pass the `Configuration` property; omitting it picks the wrong target frameworks.
41+
42+
```shell
43+
dotnet restore -p:Configuration=UniversalGLRelease
44+
```
45+
46+
## Step 4 — Build
47+
48+
```shell
49+
dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore
50+
```
51+
52+
A successful build ends with `0 Error(s)`.

.github/copilot-instructions.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Agent Instructions
2+
3+
4+
## General information
5+
6+
### Project structure
7+
8+
| Path | Description |
9+
|------|-------------|
10+
| `DXMainClient/` | Main entry-point project — always the build target |
11+
| `ClientCore/` | Core game-client logic |
12+
| `ClientGUI/` | UI layer |
13+
| `ClientUpdater/` | Auto-updater logic |
14+
| `SecondStageUpdater/` | Secondary updater executable |
15+
| `Rampastring.XNAUI/` | UI framework (git submodule) |
16+
| `GitVersion.yml` | GitVersion branch and versioning strategy |
17+
| `global.json` | Pins the required .NET SDK version (10.0, any feature band) |
18+
| `Directory.Build.props` | MSBuild properties shared across all projects |
19+
| `Directory.Packages.props` | Central NuGet package version management |
20+
| `Docs/Build.md` | Human-oriented build documentation |
21+
22+
### Build the project
23+
24+
```shell
25+
dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0
26+
```
27+
28+
A successful build ends with `0 Error(s)`.
29+
30+
### Contributing guidelines
31+
See [Contributing.md](../Contributing.md) for coding style, formatting, and other contribution guidelines. Be aware, Copilot, you MUST read and follow this file, even if the user did not explicitly ask you to.
32+
33+
## GitHub Copilot coding agent setup instructions
34+
35+
This section only applies to the GitHub Copilot coding agent, running in a Linux runner from the GitHub Action environment. It does not apply to other environments, such as local development.
36+
37+
The steps in the [copilot-coding-agent-setup.md](./copilot-coding-agent-setup.md) file are automatically executed via a GitHub Action workflow before the agent starts. **Only read and run them manually if you encounter a build failure**.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Copilot setup steps
2+
3+
# Automatically run the setup steps when they are changed to allow for easy validation, and
4+
# allow manual testing through the repository's "Actions" tab
5+
on:
6+
workflow_dispatch:
7+
push:
8+
paths:
9+
- .github/workflows/copilot-setup-steps.yml
10+
pull_request:
11+
paths:
12+
- .github/workflows/copilot-setup-steps.yml
13+
14+
jobs:
15+
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
16+
copilot-setup-steps:
17+
runs-on: ubuntu-latest
18+
19+
permissions:
20+
contents: read
21+
22+
steps:
23+
- name: Checkout code with full history and submodules
24+
uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
27+
submodules: recursive
28+
29+
# 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
30+
- name: Unshallow clone and fetch develop branch
31+
run: |
32+
git fetch --unshallow origin || true
33+
git remote set-branches origin '*'
34+
git fetch origin develop
35+
# Apply the same fix to every submodule (including nested ones), because GitVersion.MsBuild
36+
# runs against each submodule directory that carries it, and the same LibGit2Sharp refspec
37+
# crash occurs there when the submodule was checked out with a narrow single-branch refspec.
38+
git submodule foreach --recursive \
39+
'git fetch --unshallow origin || true; git remote set-branches origin "*"; git fetch origin'
40+
41+
- name: Set up .NET SDK
42+
uses: actions/setup-dotnet@v4
43+
with:
44+
global-json-file: ./global.json
45+
46+
- name: Restore NuGet packages
47+
run: dotnet restore -p:Configuration=UniversalGLRelease
48+
49+
- name: Build
50+
run: dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore

ClientCore/ClientConfiguration.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,13 @@ private List<TranslationGameFile> ParseTranslationGameFiles()
365365

366366
public string KeyboardINI => clientDefinitionsIni.GetStringValue(SETTINGS, "KeyboardINI", "Keyboard.ini");
367367

368+
public bool SettingsIniAsKeyboardIni => SettingsIniName == KeyboardINI;
369+
370+
public string KeyboardHotkeySection => clientDefinitionsIni.GetStringValue(
371+
SETTINGS,
372+
"KeyboardHotkeySection",
373+
ClientGameType == ClientType.RA ? "WinHotKeys" : "Hotkey");
374+
368375
public int MinimumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameWidth", 640);
369376

370377
public int MinimumIngameHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameHeight", 480);

ClientCore/I18N/Translation.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,48 @@ public static string GetLanguageName(string localeCode)
184184
return result;
185185
}
186186

187+
/// <summary>
188+
/// Applies (hard-links or copies) the translation game files for a given locale to the game directory,
189+
/// and removes any destination files whose source no longer exists.
190+
/// </summary>
191+
public void ApplyTranslationGameFiles() => ApplyTranslationGameFiles(LocaleCode);
192+
193+
/// <inheritdoc cref="ApplyTranslationGameFiles()"/>
194+
/// <param name="localeCode">The locale code identifying the translation whose game files should be applied.</param>
195+
public static void ApplyTranslationGameFiles(string localeCode)
196+
{
197+
ClientConfiguration.Instance.RefreshTranslationGameFiles();
198+
199+
string translationFolderPath = SafePath.CombineDirectoryPath(
200+
ClientConfiguration.Instance.TranslationsFolderPath, localeCode);
201+
202+
foreach (var tgf in ClientConfiguration.Instance.TranslationGameFiles)
203+
{
204+
string sourcePath = SafePath.CombineFilePath(translationFolderPath, tgf.Source);
205+
string targetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, tgf.Target);
206+
207+
if (File.Exists(sourcePath))
208+
{
209+
string sourceHash = Utilities.CalculateSHA1ForFile(sourcePath);
210+
string targetHash = Utilities.CalculateSHA1ForFile(targetPath);
211+
212+
if (sourceHash != targetHash)
213+
{
214+
FileExtensions.CreateHardLinkFromSource(sourcePath, targetPath);
215+
new FileInfo(targetPath).IsReadOnly = true;
216+
}
217+
}
218+
else
219+
{
220+
if (File.Exists(targetPath))
221+
{
222+
new FileInfo(targetPath).IsReadOnly = false;
223+
File.Delete(targetPath);
224+
}
225+
}
226+
}
227+
}
228+
187229
/// <summary>
188230
/// Lists valid available translations from the <see cref="TranslationsFolderPath"/> along with their UI names.
189231
/// A localization is valid if it has a corresponding <see cref="TranslationIniName"/> file in the <see cref="TranslationsFolderPath"/>.

ClientCore/Settings/UserINISettings.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,22 @@ protected UserINISettings(IniFile iniFile)
9393
else
9494
BackBufferInVRAM = new BoolSetting(iniFile, VIDEO, "VideoBackBuffer", false);
9595

96-
IngameScreenWidth = new IntSetting(iniFile, VIDEO, "ScreenWidth", 1024);
97-
IngameScreenHeight = new IntSetting(iniFile, VIDEO, "ScreenHeight", 768);
96+
IngameScreenWidth = new IntSetting(
97+
iniFile,
98+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,
99+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Width" : "ScreenWidth",
100+
1024);
101+
102+
IngameScreenHeight = new IntSetting(
103+
iniFile,
104+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,
105+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Height" : "ScreenHeight",
106+
768);
107+
98108
ClientTheme = new StringSetting(iniFile, MULTIPLAYER, "Theme", ClientConfiguration.Instance.GetThemeInfoFromIndex(0).Name);
99109
Translation = new StringSetting(iniFile, OPTIONS, "Translation", I18N.Translation.GetDefaultTranslationLocaleCode());
110+
TranslationGameFilesVersion = new StringSetting(iniFile, OPTIONS, nameof(TranslationGameFilesVersion), string.Empty);
111+
100112
DetailLevel = new IntSetting(iniFile, OPTIONS, "DetailLevel", 2);
101113
Renderer = new StringSetting(iniFile, COMPATIBILITY, "Renderer", string.Empty);
102114
WindowedMode = new BoolSetting(iniFile, VIDEO, ClientConfiguration.Instance.WindowedModeKey, false);
@@ -106,8 +118,17 @@ protected UserINISettings(IniFile iniFile)
106118
ClientFPS = new IntSetting(iniFile, VIDEO, "ClientFPS", 60);
107119
DisplayToggleableExtraTextures = new BoolSetting(iniFile, VIDEO, "DisplayToggleableExtraTextures", true);
108120

109-
ScoreVolume = new DoubleSetting(iniFile, AUDIO, "ScoreVolume", 0.7);
110-
SoundVolume = new DoubleSetting(iniFile, AUDIO, "SoundVolume", 0.7);
121+
// RA1 reads MultiplayerScoreVolume instead of ScoreVolume. This value is handled when saving
122+
ScoreVolume = new DoubleSetting(iniFile,
123+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,
124+
"ScoreVolume",
125+
0.7);
126+
127+
SoundVolume = new DoubleSetting(iniFile,
128+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,
129+
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Volume" : "SoundVolume",
130+
0.7);
131+
111132
VoiceVolume = new DoubleSetting(iniFile, AUDIO, "VoiceVolume", 0.7);
112133
IsScoreShuffle = new BoolSetting(iniFile, AUDIO, "IsScoreShuffle", true);
113134
ClientVolume = new DoubleSetting(iniFile, AUDIO, "ClientVolume", 1.0);
@@ -176,9 +197,11 @@ protected UserINISettings(IniFile iniFile)
176197

177198
public IntSetting IngameScreenWidth { get; private set; }
178199
public IntSetting IngameScreenHeight { get; private set; }
200+
179201
public StringSetting ClientTheme { get; private set; }
180202
public string ThemeFolderPath => ClientConfiguration.Instance.GetThemePath(ClientTheme);
181203
public StringSetting Translation { get; private set; }
204+
public StringSetting TranslationGameFilesVersion { get; private set; }
182205
public string TranslationFolderPath => SafePath.CombineDirectoryPath(
183206
ClientConfiguration.Instance.TranslationsFolderPath, Translation);
184207
public string TranslationThemeFolderPath => SafePath.CombineDirectoryPath(
@@ -447,6 +470,10 @@ public void SaveSettings()
447470
ApplyDefaults();
448471
// CleanUpLegacySettings();
449472

473+
// RA1 reads MultiplayerScoreVolume instead of ScoreVolume
474+
if (ClientConfiguration.Instance.ClientGameType == ClientType.RA)
475+
SettingsIni.SetDoubleValue(OPTIONS, "MultiplayerScoreVolume", SettingsIni.GetDoubleValue(OPTIONS, "ScoreVolume", 0.7));
476+
450477
SettingsIni.WriteIniFile();
451478

452479
SettingsSaved?.Invoke(this, EventArgs.Empty);

ClientGUI/HotkeyConfigurationWindow.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using ClientCore.Extensions;
22
using ClientCore;
3+
34
using Microsoft.Xna.Framework;
45
using Microsoft.Xna.Framework.Input;
6+
57
using Rampastring.Tools;
68
using Rampastring.XNAUI;
79
using Rampastring.XNAUI.XNAControls;
10+
811
using System;
912
using System.Collections.Generic;
1013

@@ -16,7 +19,6 @@ namespace ClientGUI
1619
public class HotkeyConfigurationWindow : XNAWindow
1720
{
1821
private readonly string HOTKEY_TIP_TEXT = "Press a key...".L10N("Client:DTAConfig:PressAKey");
19-
private const string HOTKEY_INI_SECTION = "Hotkey";
2022
private const string KEYBOARD_COMMANDS_INI = "KeyboardCommands.ini";
2123

2224
public HotkeyConfigurationWindow(WindowManager windowManager) : base(windowManager)
@@ -292,13 +294,17 @@ private void GameProcessLogic_GameProcessExited()
292294

293295
private void LoadKeyboardINI()
294296
{
295-
keyboardINI = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI));
297+
keyboardINI = ClientConfiguration.Instance.SettingsIniAsKeyboardIni
298+
? UserINISettings.Instance.SettingsIni
299+
: new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI));
296300

297-
if (SafePath.GetFile(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI).Exists)
301+
if (ClientConfiguration.Instance.SettingsIniAsKeyboardIni
302+
? keyboardINI.SectionExists(ClientConfiguration.Instance.KeyboardHotkeySection)
303+
: SafePath.GetFile(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI).Exists)
298304
{
299305
foreach (var command in gameCommands)
300306
{
301-
int hotkey = keyboardINI.GetIntValue("Hotkey", command.ININame, 0);
307+
int hotkey = keyboardINI.GetIntValue(ClientConfiguration.Instance.KeyboardHotkeySection, command.ININame, 0);
302308

303309
Hotkey hotkeyStruct = new Hotkey(hotkey);
304310
command.Hotkey = new Hotkey(GetKeyOverride(hotkeyStruct.Key), hotkeyStruct.Modifier);
@@ -482,13 +488,21 @@ private KeyModifiers GetCurrentModifiers()
482488

483489
private void WriteKeyboardINI()
484490
{
485-
var keyboardIni = new IniFile();
491+
var keyboardIni = ClientConfiguration.Instance.SettingsIniAsKeyboardIni
492+
? UserINISettings.Instance.SettingsIni
493+
: new IniFile();
494+
486495
foreach (var command in gameCommands)
487496
{
488-
keyboardIni.SetStringValue("Hotkey", command.ININame, command.Hotkey.GetTSEncoded().ToString());
497+
keyboardIni.SetStringValue(ClientConfiguration.Instance.KeyboardHotkeySection, command.ININame, command.Hotkey.GetTSEncoded().ToString());
489498
}
490499

491-
keyboardIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI));
500+
// Do not write INI file if using Settings.ini as Keyboard.ini. The hot keys will be saved when Settings.ini is saved.
501+
// 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.
502+
// If we don't flush here, everything can be restored by hitting a cancel.
503+
// If we flush here -- the player can't cancel anymore at all.
504+
if (!ClientConfiguration.Instance.SettingsIniAsKeyboardIni)
505+
keyboardIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI));
492506
}
493507

494508
/// <summary>

0 commit comments

Comments
 (0)