Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ClientCore/ClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ public void RefreshSettings()

public string[] AlwaysTrustedDomains = {"cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "discord.gg", "youtube.com", "youtu.be"};

public bool ShowGameIconInGameList => clientDefinitionsIni.GetBooleanValue(SETTINGS, "ShowGameIconInGameList", true);

public (string Name, string Path) GetThemeInfoFromIndex(int themeIndex) => clientDefinitionsIni.GetStringValue("Themes", themeIndex.ToString(), ",").Split(',').AsTuple2();

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion ClientCore/ProgramConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static class ProgramConstants

public const string QRES_EXECUTABLE = "qres.dat";

public const string CNCNET_PROTOCOL_REVISION = "R13";
public const string CNCNET_PROTOCOL_REVISION = "R14";
public const string LAN_PROTOCOL_REVISION = "RL8";
public const int LAN_PORT = 1234;
public const int LAN_INGAME_PORT = 1234;
Expand Down
52 changes: 51 additions & 1 deletion ClientCore/Settings/UserINISettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class UserINISettings
public const string AUDIO = "Audio";
public const string COMPATIBILITY = "Compatibility";
public const string GAME_FILTERS = "GameFilters";
public const string GAME_OPTION_FILTERS = "GameOptionFilters";
private const string FAVORITE_MAPS = "FavoriteMaps";

private const bool DEFAULT_SHOW_FRIENDS_ONLY_GAMES = false;
Expand Down Expand Up @@ -267,6 +268,34 @@ protected UserINISettings(IniFile iniFile)

public IntRangeSetting MaxPlayerCount { get; private set; }

/************************/
/* GAME OPTION FILTERS */
/************************/

/// <summary>
/// Gets the filter value for a checkbox game option.
/// Returns 0 for "All", 1 for "On", 2 for "Off".
/// </summary>
public int GetCheckboxFilterValue(string optionName) => SettingsIni.GetIntValue(GAME_OPTION_FILTERS, optionName, 0);

/// <summary>
/// Sets the filter value for a checkbox game option.
/// 0 = "All", 1 = "On", 2 = "Off".
/// </summary>
public void SetCheckboxFilterValue(string optionName, int value) => SettingsIni.SetIntValue(GAME_OPTION_FILTERS, optionName, value);

/// <summary>
/// Gets the filter value for a dropdown game option.
/// Returns -1 for "All", or the selected index.
/// </summary>
public int GetDropdownFilterValue(string optionName) => SettingsIni.GetIntValue(GAME_OPTION_FILTERS, optionName, -1);

/// <summary>
/// Sets the filter value for a dropdown game option.
/// -1 = "All", or the selected index.
/// </summary>
public void SetDropdownFilterValue(string optionName, int value) => SettingsIni.SetIntValue(GAME_OPTION_FILTERS, optionName, value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This is a bit inconsistent, I think -1 should mean "irrelevant" in both the dropdown and the checkbox.
  2. I am not sure yet, but to me int? and bool? sounds like a more obvious choice here. Curious to hear what you think about that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True - nullable is better. Fixed in dda5b0c.


/********/
/* MISC */
/********/
Expand Down Expand Up @@ -424,7 +453,8 @@ public bool IsGameFiltersApplied()
|| HideLockedGames.Value != DEFAULT_HIDE_LOCKED_GAMES
|| HidePasswordedGames.Value != DEFAULT_HIDE_PASSWORDED_GAMES
|| HideIncompatibleGames.Value != DEFAULT_HIDE_INCOMPATIBLE_GAMES
|| MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT;
|| MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT
|| HasGameOptionFilters();

public void ResetGameFilters()
{
Expand All @@ -433,6 +463,26 @@ public void ResetGameFilters()
HideIncompatibleGames.Value = DEFAULT_HIDE_INCOMPATIBLE_GAMES;
HidePasswordedGames.Value = DEFAULT_HIDE_PASSWORDED_GAMES;
MaxPlayerCount.Value = DEFAULT_MAX_PLAYER_COUNT;
ResetGameOptionFilters();
}

/// <summary>
/// Checks if any game option filters are set.
/// </summary>
private bool HasGameOptionFilters()
{
var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);
return section != null && section.Keys.Count > 0;
}

/// <summary>
/// Clears all game option filters.
/// </summary>
private void ResetGameOptionFilters()
{
var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);
if (section != null)
section.RemoveAllKeys();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

section?.RemoveAllKeys();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks.

}

/// <summary>
Expand Down
59 changes: 58 additions & 1 deletion DXMainClient/DXGUI/Generic/GameSessionCheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,43 @@ public bool AllowScoring

private string enabledSpawnIniValue = "True";
private string disabledSpawnIniValue = "False";


/// <summary>
/// Whether this checkbox should be included in the GAME broadcast.
/// </summary>
public bool BroadcastToLobby { get; private set; }

/// <summary>
/// Whether the icon should be shown in the game list.
/// </summary>
public bool IconShownInGameList { get; private set; }

/// <summary>
/// Whether the icon should be shown on the right side of the game list.
/// Only applies if IconShownInGameList is true.
/// </summary>
public bool IconShownInGameListOnRight { get; private set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever customisation!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Perhaps it would be better to have both "ShownInGameListLHS" and "ShownInGameListRHS". What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, is there a use for having both?


/// <summary>
/// Whether the icon should be shown in the game information panel.
/// </summary>
public bool IconShownInGameInfo { get; private set; }

/// <summary>
/// Whether the icon should be shown in the game filters panel.
/// </summary>
public bool IconShownInFilters { get; private set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I know I said previously that it would be good to have it customisable, but I am not sure now. Do you think there would be any case when the modder or game maintainer would want to have an icon in game list but not have the icon anywhere else? ATM I am not sure if this makes sense, because the game info/filters icon work as legend, so probably could omit that.

Also I think we could draw the icons near game options in the lobby themselves the same way?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we keep the settings but make it apply to the whole thing not just the icon. So "ShownInGameInfo" instead of "IconShownInGameInfo", etc. It's programmed a bit like that already - hence the missing Credits in that screenshot.

I was having hassles with icons in the game lobby. I'll take another look.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the filter and the game info should be switched on by the same option, otherwise it would be pretty user unfriendly to have something in filter but not in game info, and vice versa. Same for icons in the list - if the icon is shown in the game list - it must have the same icon (working as legend) in filter and in game info, so the users don't get confused on the meaning.


/// <summary>
/// The texture name for the icon when setting is enabled.
/// </summary>
public string EnabledIcon { get; private set; }

/// <summary>
/// The texture name for the icon when setting is disabled.
/// </summary>
public string DisabledIcon { get; private set; }

protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)
{
switch (key)
Expand Down Expand Up @@ -85,6 +121,27 @@ protected override void ParseControlINIAttribute(IniFile iniFile, string key, st
case "MapScoringMode":
mapScoringMode = (CheckBoxMapScoringMode)Enum.Parse(typeof(CheckBoxMapScoringMode), value);
return;
case "BroadcastToLobby":
BroadcastToLobby = Conversions.BooleanFromString(value, false);
return;
case "IconShownInGameList":
IconShownInGameList = Conversions.BooleanFromString(value, false);
return;
case "IconShownInGameListOnRight":
IconShownInGameListOnRight = Conversions.BooleanFromString(value, false);
return;
case "IconShownInGameInfo":
IconShownInGameInfo = Conversions.BooleanFromString(value, false);
return;
case "IconShownInFilters":
IconShownInFilters = Conversions.BooleanFromString(value, false);
return;
case "EnabledIcon":
EnabledIcon = value;
return;
case "DisabledIcon":
DisabledIcon = value;
return;
}

base.ParseControlINIAttribute(iniFile, key, value);
Expand Down
32 changes: 32 additions & 0 deletions DXMainClient/DXGUI/Generic/GameSessionDropDown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ public GameSessionDropDown(WindowManager windowManager) : base(windowManager) {

private int defaultIndex;

/// <summary>
/// Whether this dropdown should be included in the GAME broadcast.
/// </summary>
public bool BroadcastToLobby { get; private set; }

/// <summary>
/// Whether the icon should be shown in the game information panel.
/// </summary>
public bool IconShownInGameInfo { get; private set; }

/// <summary>
/// Whether the icon should be shown in the game filters panel.
/// </summary>
public bool IconShownInFilters { get; private set; }

/// <summary>
/// The texture name for the icon.
/// </summary>
public string Icon { get; private set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for this to be more useful, we have to have not one icon per dropdown but actually an icon per option in the dropdown. If the icon is missing -- then no icon would be shown.

This would also streamline the behavior between the checkbox and the dropdown. The icon could be then drawn as ItemTexture or however the option was called.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks and fixed.


protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)
{
// shorthand for localization function
Expand Down Expand Up @@ -76,6 +96,18 @@ static string Localize(XNAControl control, string attributeName, string defaultV
case "OptionName":
OptionName = Localize(this, "OptionName", value);
return;
case "BroadcastToLobby":
BroadcastToLobby = Conversions.BooleanFromString(value, false);
return;
case "IconShownInGameInfo":
IconShownInGameInfo = Conversions.BooleanFromString(value, false);
return;
case "IconShownInFilters":
IconShownInFilters = Conversions.BooleanFromString(value, false);
return;
case "Icon":
Icon = value;
return;
}

base.ParseControlINIAttribute(iniFile, key, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,9 @@ private void BroadcastGame()
sb.Append(0); // LoadedGameId
sb.Append(";");
sb.Append(ClientConfiguration.Instance.DefaultSkillLevelIndex); // we don't know the original skill level
sb.Append(";");
sb.Append(string.Empty); // MapHash
sb.Append(";"); // Map SHA1
sb.Append(";"); // Packed checkbox values (empty for loaded lobbies)
sb.Append(";"); // Dropdown indices (empty for loaded lobbies)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to separate dropdowns and indices like this? They are essentially both the same, checkbox is an option with 0, 1 states, and dropdown has 0, 1, 2, ... etc. I've created an interface for game settings specifically to generalize this kind of behavior. What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. That stuff was mostly copied from the existing game options broadcast which had it split. See:

// We don't gain much in most cases by packing the drop-down values

RE the interface - makes sense. I was working on an outdated copy and didn't see those changes. Hopefully 60b23c5 sorts it as you imagined.


broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20);
}
Expand Down
110 changes: 107 additions & 3 deletions DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ public override void Initialize()
btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 47
);

panelGameFilters = new GameFiltersPanel(WindowManager);
panelGameFilters = new GameFiltersPanel(WindowManager, gameLobby);
panelGameFilters.Name = nameof(panelGameFilters);
panelGameFilters.ClientRectangle = gameListRectangle;
panelGameFilters.Disable();

lbGameList = new GameListBox(WindowManager, mapLoader, localGameID, HostedGameMatches);
lbGameList = new GameListBox(WindowManager, mapLoader, localGameID, gameLobby, HostedGameMatches);
lbGameList.Name = nameof(lbGameList);
lbGameList.ClientRectangle = gameListRectangle;
lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED;
Expand Down Expand Up @@ -421,6 +421,12 @@ private void TbGameSearch_InputReceived(object sender, EventArgs e)
lbGameList.ViewTop = 0;
}


/// <summary>
/// Checks if a hosted game matches the current filter criteria.
/// </summary>
/// <param name="hg">The hosted game to check.</param>
/// <returns>True if the game matches the filter criteria, false otherwise.</returns>
private bool HostedGameMatches(GenericHostedGame hg)
{
// friends list takes priority over other filters below
Expand All @@ -439,6 +445,9 @@ private bool HostedGameMatches(GenericHostedGame hg)
if (hg.MaxPlayers > UserINISettings.Instance.MaxPlayerCount.Value)
return false;

if (hg is HostedCnCNetGame cncnetGame && !GameOptionsMatch(cncnetGame))
return false;

string textUpper = tbGameSearch?.Text?.ToUpperInvariant();

string translatedGameMode = string.IsNullOrEmpty(hg.GameMode)
Expand All @@ -460,6 +469,62 @@ private bool HostedGameMatches(GenericHostedGame hg)
hg.Players.Any(pl => pl.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal));
}

/// <summary>
/// Checks if a game's broadcast options match the current filter criteria.
/// </summary>
/// <param name="game">The hosted game to check.</param>
/// <returns>True if the game matches the filter criteria, false otherwise.</returns>
private bool GameOptionsMatch(HostedCnCNetGame game)
{
// checkbox filters
var broadcastableCheckboxes = gameLobby.CheckBoxes.Where(cb => cb.BroadcastToLobby).ToList();
if (game.BroadcastedCheckboxValues != null && broadcastableCheckboxes.Count > 0)
{
for (int i = 0; i < broadcastableCheckboxes.Count; i++)
{
if (i >= game.BroadcastedCheckboxValues.Length)
break;

var checkbox = broadcastableCheckboxes[i];
int filterValue = UserINISettings.Instance.GetCheckboxFilterValue(checkbox.Name);

// 0 = All, 1 = On, 2 = Off
if (filterValue == 0)
continue;

bool isChecked = game.BroadcastedCheckboxValues[i];

bool shouldBeChecked = filterValue == 1;
if (isChecked != shouldBeChecked)
return false;
}
}

// dropdown filters
var broadcastableDropdowns = gameLobby.DropDowns.Where(dd => dd.BroadcastToLobby).ToList();
if (game.BroadcastedDropdownIndices != null && broadcastableDropdowns.Count > 0)
{
for (int i = 0; i < broadcastableDropdowns.Count; i++)
{
if (i >= game.BroadcastedDropdownIndices.Length)
break;

var dropdown = broadcastableDropdowns[i];
int filterValue = UserINISettings.Instance.GetDropdownFilterValue(dropdown.Name);

// -1 = All, otherwise check for exact match
if (filterValue == -1)
continue;

int gameSelectedIndex = game.BroadcastedDropdownIndices[i];

if (gameSelectedIndex != filterValue)
return false;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be deduplicated I think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

60b23c5 sorts it.


return true;
}

private void OnCnCNetGameCountUpdated(object sender, PlayerCountEventArgs e) => UpdateOnlineCount(e.PlayerCount);

Expand Down Expand Up @@ -1504,7 +1569,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr
string msg = e.Message.Substring(5); // Cut out GAME part
string[] splitMessage = msg.Split(new char[] { ';' });

if (splitMessage.Length != 13)
if (splitMessage.Length != 15)
{
Logger.Log("Ignoring CTCP game message because of an invalid amount of parameters.");

Expand Down Expand Up @@ -1551,6 +1616,43 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr
int skillLevel = int.Parse(splitMessage[11]);
string mapHash = splitMessage[12];

bool[] checkboxValues = null;
int[] dropdownIndices = null;

// Games with different versions may have different option counts, so ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not only with different versions, but the LocalGame can be different too, as the client has the ability to monitor other LocalGames game listings

if (gameVersion == ProgramConstants.GAME_VERSION)
{
// packed checkbox values
if (!string.IsNullOrEmpty(splitMessage[13]))
{
string[] checkboxStrings = splitMessage[13].Split(',');
int[] packedCheckboxes = new int[checkboxStrings.Length];
for (int i = 0; i < checkboxStrings.Length; i++)
packedCheckboxes[i] = int.Parse(checkboxStrings[i]);

int localCheckboxCount = gameLobby.CheckBoxes.Count(cb => cb.BroadcastToLobby);
checkboxValues = new bool[localCheckboxCount];
for (int i = 0; i < localCheckboxCount; i++)
{
int packedIndex = i / 32;
int bitIndex = i % 32;
if (packedIndex < packedCheckboxes.Length)
checkboxValues[i] = (packedCheckboxes[packedIndex] & (1 << bitIndex)) != 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain what is happening here? I see bitpacking which is a clever optimisation, but apart from that I have no idea

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// dropdown indices
if (!string.IsNullOrEmpty(splitMessage[14]))
{
string[] dropdownStrings = splitMessage[14].Split(',');
int localDropdownCount = gameLobby.DropDowns.Count(dd => dd.BroadcastToLobby);
int dropdownCount = Math.Min(dropdownStrings.Length, localDropdownCount);
dropdownIndices = new int[dropdownCount];
for (int i = 0; i < dropdownCount; i++)
dropdownIndices[i] = int.Parse(dropdownStrings[i]);
}
}

CnCNetGame cncnetGame = gameCollection.GameList.Find(g => g.GameBroadcastChannel == channel.ChannelName);

if (cncnetGame == null)
Expand Down Expand Up @@ -1611,6 +1713,8 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr
game.Incompatible = cncnetGame == localGame && game.GameVersion != ProgramConstants.GAME_VERSION;
game.TunnelServer = tunnel;
game.SkillLevel = skillLevel;
game.BroadcastedCheckboxValues = checkboxValues;
game.BroadcastedDropdownIndices = dropdownIndices;

if (isClosed)
{
Expand Down
Loading
Loading