From 1d40b86b661e0f2a3dd255f5b9142f8ba203e0bd Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 22 Jun 2026 12:20:02 +0100 Subject: [PATCH 1/6] Enforce valid labels for game objects in C++ --- src/games/game.h | 89 ++++++++++++++++++++++++++++++++++++--- src/pygambit/action.pxi | 7 ++- src/pygambit/gambit.pxd | 14 +++--- src/pygambit/infoset.pxi | 7 ++- src/pygambit/node.pxi | 7 ++- src/pygambit/outcome.pxi | 7 ++- src/pygambit/player.pxi | 7 ++- src/pygambit/strategy.pxi | 7 ++- 8 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 6d77077818..03dccc2854 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -111,6 +111,58 @@ class InvalidFileException : public std::runtime_error { ~InvalidFileException() noexcept override = default; }; +//======================================================================= +// Validation of labels +//======================================================================= + +/// @brief Returns whether p_label is a valid label for a game object. +/// +/// A valid label either is the empty string (denoting the absence of a label), +/// or consists only of printable ASCII characters and spaces, begins and ends +/// with a printable character, and contains no two consecutive spaces. +/// +/// @note The set of valid labels is intended to be widened to permit Unicode in +/// a future version; this function is the single point at which the +/// definition is enforced. +inline bool IsValidLabel(const std::string &p_label) +{ + if (p_label.empty()) { + return true; + } + auto is_printable = [](unsigned char c) { return c >= 0x21 && c <= 0x7e; }; + if (!is_printable(p_label.front()) || !is_printable(p_label.back())) { + return false; + } + bool previous_was_space = false; + for (const char ch : p_label) { + const auto c = static_cast(ch); + if (c == ' ') { + if (previous_was_space) { + return false; // two consecutive spaces + } + previous_was_space = true; + } + else if (is_printable(c)) { + previous_was_space = false; + } + else { + return false; // tab, newline, other control, or non-ASCII byte + } + } + return true; +} + +/// @brief Throws ValueException if p_label is not a valid label. +/// @sa IsValidLabel +inline void CheckLabel(const std::string &p_label) +{ + if (!IsValidLabel(p_label)) { + throw ValueException("Invalid label: a label may contain only printable ASCII " + "characters and spaces, must not begin or end with a space, " + "and must not contain two consecutive spaces"); + } +} + //======================================================================= // Classes representing objects in a game //======================================================================= @@ -149,7 +201,11 @@ class GameOutcomeRep : public std::enable_shared_from_this { /// Returns the text label associated with the outcome const std::string &GetLabel() const { return m_label; } /// Sets the text label associated with the outcome - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } /// Gets the payoff associated with the outcome to the player template const T &GetPayoff(const GamePlayer &p_player) const; @@ -184,7 +240,11 @@ class GameActionRep : public std::enable_shared_from_this { GameInfoset GetInfoset() const; const std::string &GetLabel() const { return m_label; } - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } bool Precedes(const GameNode &) const; }; @@ -229,7 +289,11 @@ class GameInfosetRep : public std::enable_shared_from_this { bool IsChanceInfoset() const; - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } const std::string &GetLabel() const { return m_label; } /// @name Actions @@ -297,6 +361,7 @@ class GameStrategyRep : public std::enable_shared_from_this { explicit GameStrategyRep(GamePlayerRep *p_player, int p_number, const std::string &p_label) : m_player(p_player), m_number(p_number), m_label(p_label) { + CheckLabel(p_label); } //@} @@ -308,7 +373,11 @@ class GameStrategyRep : public std::enable_shared_from_this { /// Returns the text label associated with the strategy const std::string &GetLabel() const { return m_label; } /// Sets the text label associated with the strategy - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } /// Returns the game on which the strategy is defined Game GetGame() const; @@ -406,7 +475,11 @@ class GamePlayerRep : public std::enable_shared_from_this { Game GetGame() const; const std::string &GetLabel() const { return m_label; } - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } bool IsChance() const { return (m_number == 0); } @@ -481,7 +554,11 @@ class GameNodeRep : public std::enable_shared_from_this { Game GetGame() const; const std::string &GetLabel() const { return m_label; } - void SetLabel(const std::string &p_label) { m_label = p_label; } + void SetLabel(const std::string &p_label) + { + CheckLabel(p_label); + m_label = p_label; + } int GetNumber() const; GameNode GetChild(const GameAction &p_action) diff --git a/src/pygambit/action.pxi b/src/pygambit/action.pxi index 48d1d7ee7b..7964ff2a44 100644 --- a/src/pygambit/action.pxi +++ b/src/pygambit/action.pxi @@ -72,7 +72,12 @@ class Action: @property def label(self) -> str: - """Get or set the text label of the action.""" + """Get or set the text label of the action. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.action.deref().GetLabel().decode("ascii") @label.setter diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 62e83e8312..099e4aa2a6 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -106,7 +106,7 @@ cdef extern from "games/game.h": int GetId() except + c_GamePlayer GetPlayer() except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError c_GameAction GetAction(c_GameInfoset) except + cdef cppclass c_GameSequenceRep "GameSequenceRep": @@ -120,7 +120,7 @@ cdef extern from "games/game.h": bint Precedes(c_GameNode) except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError cdef cppclass c_GameInfosetRep "GameInfosetRep": cppclass Actions: @@ -148,7 +148,7 @@ cdef extern from "games/game.h": c_GamePlayer GetPlayer() except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError c_GameAction GetAction(int) except +IndexError Actions GetActions() except + @@ -197,7 +197,7 @@ cdef extern from "games/game.h": int IsChance() except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError c_GameStrategy GetStrategy(int) except +IndexError Strategies GetStrategies() except + @@ -212,7 +212,7 @@ cdef extern from "games/game.h": int GetNumber() except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError T GetPayoff[T](c_GamePlayer) except +IndexError void SetPayoff(c_GamePlayer, c_Number) except +IndexError @@ -232,7 +232,7 @@ cdef extern from "games/game.h": int GetNumber() except + string GetLabel() except + - void SetLabel(string) except + + void SetLabel(string) except +ValueError c_GameInfoset GetInfoset() except + c_GamePlayer GetPlayer() except + @@ -333,7 +333,7 @@ cdef extern from "games/game.h": Nodes GetNodes() except + c_GameStrategy GetStrategy(int) except +IndexError - c_GameStrategy NewStrategy(c_GamePlayer, string) except + + c_GameStrategy NewStrategy(c_GamePlayer, string) except +ValueError void DeleteStrategy(c_GameStrategy) except + int MixedProfileLength() except + diff --git a/src/pygambit/infoset.pxi b/src/pygambit/infoset.pxi index 82a0f7098e..7ee046a7d6 100644 --- a/src/pygambit/infoset.pxi +++ b/src/pygambit/infoset.pxi @@ -165,7 +165,12 @@ class Infoset: @property def label(self) -> str: - """Get or set the text label of the information set.""" + """Get or set the text label of the information set. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.infoset.deref().GetLabel().decode("ascii") @label.setter diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index ec30b40bf4..8a4a64278e 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -135,7 +135,12 @@ class Node: @property def label(self) -> str: - """The text label associated with the node.""" + """The text label associated with the node. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.node.deref().GetLabel().decode("ascii") @label.setter diff --git a/src/pygambit/outcome.pxi b/src/pygambit/outcome.pxi index 6f3e62f232..d8cc806e94 100644 --- a/src/pygambit/outcome.pxi +++ b/src/pygambit/outcome.pxi @@ -63,7 +63,12 @@ class Outcome: @property def label(self) -> str: - """The text label associated with this outcome.""" + """The text label associated with this outcome. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.outcome.deref().GetLabel().decode("ascii") @label.setter diff --git a/src/pygambit/player.pxi b/src/pygambit/player.pxi index 6c54545f49..351339d985 100644 --- a/src/pygambit/player.pxi +++ b/src/pygambit/player.pxi @@ -247,7 +247,12 @@ class Player: @property def label(self) -> str: - """Gets or sets the text label of the player.""" + """Gets or sets the text label of the player. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.player.deref().GetLabel().decode("ascii") @label.setter diff --git a/src/pygambit/strategy.pxi b/src/pygambit/strategy.pxi index fcad92db41..048a9ef52c 100644 --- a/src/pygambit/strategy.pxi +++ b/src/pygambit/strategy.pxi @@ -52,7 +52,12 @@ class Strategy: @property def label(self) -> str: - """Get or set the text label associated with the strategy.""" + """Get or set the text label associated with the strategy. + + .. versionchanged:: 16.7.0 + An invalid label now raises ``ValueError``: a label may contain only printable ASCII + characters and spaces, not begin/end with a space, nor have two consecutive spaces. + """ return self.strategy.deref().GetLabel().decode("ascii") @label.setter From c1293ec71421895a91cbb5cc6e06fb00c626b458 Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 22 Jun 2026 12:39:04 +0100 Subject: [PATCH 2/6] Encode add_strategy label as ASCII --- src/pygambit/game.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 9baaa6ff3f..2d146f7812 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2210,9 +2210,9 @@ class Game: ) resolved_player = cython.cast(Player, self._resolve_player(player, "add_strategy")) + label_bytes = (str(label) if label is not None else "").encode("ascii") return Strategy.wrap( - self.game.deref().NewStrategy(resolved_player.player, - (str(label) if label is not None else "").encode()) + self.game.deref().NewStrategy(resolved_player.player, label_bytes) ) def delete_strategy(self, strategy: Strategy | str) -> None: From 992705ec704789b24621eee5b0742b35ac840057 Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 22 Jun 2026 12:43:27 +0100 Subject: [PATCH 3/6] Test label validation across object types --- tests/games.py | 8 ++++++++ tests/test_actions.py | 22 ++++++++++++++++++++-- tests/test_infosets.py | 22 +++++++++++++++++++--- tests/test_node.py | 27 ++++++++++++++++++++++++--- tests/test_outcomes.py | 27 ++++++++++++++++++++++----- tests/test_players.py | 42 ++++++++++++++++++++++++++++++++++++------ 6 files changed, 129 insertions(+), 19 deletions(-) diff --git a/tests/games.py b/tests/games.py index 2c1636f227..872811320b 100644 --- a/tests/games.py +++ b/tests/games.py @@ -8,6 +8,14 @@ import pygambit as gbt +# Label-validation fixtures. +# VALID: accepted by the C++ validator. +# INVALID: rejected by the validator -> ValueError (reach CheckLabel as ASCII bytes). +# NON_ASCII: rejected at the pygambit ASCII encode boundary +VALID_LABELS = ["x", "a b", "a b c"] +INVALID_LABELS = [" x", "x ", " ", "a b", "a\tb", "a\nb"] +NON_ASCII_LABELS = ["é", "naïve"] + def read_from_file(fn: str) -> gbt.Game: if fn.endswith(".efg"): diff --git a/tests/test_actions.py b/tests/test_actions.py index 26ecd2ddc4..28b61eeee5 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -5,8 +5,9 @@ from . import games -@pytest.mark.parametrize("game,label", [(games.create_stripped_down_poker_efg(), "random label")]) -def test_set_action_label(game: gbt.Game, label: str): +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_set_action_label(label: str): + game = games.create_stripped_down_poker_efg() action = next(iter(game.root.infoset.actions)) action.label = label assert action.label == label @@ -24,6 +25,23 @@ def test_set_duplicate_action_futurewarning(): next(iter(game.root.infoset.actions)).label = "Queen" +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_action_label_invalid_raises_valueerror(label: str): + game = games.create_stripped_down_poker_efg() + action = next(iter(game.root.infoset.actions)) + with pytest.raises(ValueError): + action.label = label + + +@pytest.mark.parametrize("label", games.NON_ASCII_LABELS) +def test_action_label_non_ascii_rejected(label: str): + """ASCII-only for 16.7 (#944); Unicode deferred to #862 (17.0).""" + game = games.create_stripped_down_poker_efg() + action = next(iter(game.root.infoset.actions)) + with pytest.raises(UnicodeEncodeError): + action.label = label + + @pytest.mark.parametrize( "game,inprobs,outprobs", [ diff --git a/tests/test_infosets.py b/tests/test_infosets.py index ae57d9c87e..e64d9b70c3 100644 --- a/tests/test_infosets.py +++ b/tests/test_infosets.py @@ -9,10 +9,26 @@ from . import games -def test_infoset_set_label(): +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_infoset_set_label(label): game = games.read_from_file("basic_extensive_game.efg") - game.root.infoset.label = "infoset 1" - assert game.root.infoset.label == "infoset 1" + game.root.infoset.label = label + assert game.root.infoset.label == label + + +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_infoset_label_invalid_raises_valueerror(label): + game = games.read_from_file("basic_extensive_game.efg") + with pytest.raises(ValueError): + game.root.infoset.label = label + + +@pytest.mark.parametrize("label", games.NON_ASCII_LABELS) +def test_infoset_label_non_ascii_rejected(label): + """ASCII-only for 16.7 (#944); Unicode deferred to #862 (17.0).""" + game = games.read_from_file("basic_extensive_game.efg") + with pytest.raises(UnicodeEncodeError): + game.root.infoset.label = label def test_infoset_player_retrieval(): diff --git a/tests/test_node.py b/tests/test_node.py index ea148839a4..8792021c86 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -709,11 +709,11 @@ def test_append_move_actions_list_of_mixed_node_references(): node1 = game.root.children["2"].children["1"] node2 = game.root.children["1"].children["1"] - node1.label = " 000" - node_references = [" 000", node2] + node1.label = "000" + node_references = ["000", node2] game.append_move(node_references, "Player 3", ["B", "F", "S"]) - assert node1.children["B"].parent.label == " 000" + assert node1.children["B"].parent.label == "000" assert len(node1.children) == 3 assert len(node2.children) == 3 @@ -1049,6 +1049,27 @@ def test_node_children_other_infoset_action(): _ = game.root.children[game.root.children["King"].infoset.actions["Bet"]] +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_node_label_valid(label): + game = games.read_from_file("basic_extensive_game.efg") + game.root.label = label + assert game.root.label == label + + +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_node_label_invalid_raises_valueerror(label): + game = games.read_from_file("basic_extensive_game.efg") + with pytest.raises(ValueError): + game.root.label = label + + +@pytest.mark.parametrize("label", games.NON_ASCII_LABELS) +def test_node_label_non_ascii_rejected(label): + game = games.read_from_file("basic_extensive_game.efg") + with pytest.raises(UnicodeEncodeError): + game.root.label = label + + @pytest.mark.parametrize( "game_obj", [ diff --git a/tests/test_outcomes.py b/tests/test_outcomes.py index 068fe83f62..6a29d399b7 100644 --- a/tests/test_outcomes.py +++ b/tests/test_outcomes.py @@ -2,6 +2,8 @@ import pygambit as gbt +from . import games + @pytest.mark.parametrize( "game", [gbt.Game.new_table([2, 2]), gbt.Game.new_tree()] @@ -21,16 +23,31 @@ def test_outcome_delete(game: gbt.Game): assert len(game.outcomes) == outcome_count - 1 -@pytest.mark.parametrize( - "game,label", - [(gbt.Game.new_table([2, 2]), "outcome label")] -) -def test_outcome_label(game: gbt.Game, label: str): +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_outcome_label(label: str): + game = gbt.Game.new_table([2, 2]) outcome = next(iter(game.outcomes)) outcome.label = label assert outcome.label == label +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_outcome_label_invalid_raises_valueerror(label: str): + game = gbt.Game.new_table([2, 2]) + outcome = next(iter(game.outcomes)) + with pytest.raises(ValueError): + outcome.label = label + + +@pytest.mark.parametrize("label", games.NON_ASCII_LABELS) +def test_outcome_label_non_ascii_rejected(label: str): + """ASCII-only for 16.7 (#944); Unicode deferred to #862 (17.0).""" + game = gbt.Game.new_table([2, 2]) + outcome = next(iter(game.outcomes)) + with pytest.raises(UnicodeEncodeError): + outcome.label = label + + @pytest.mark.parametrize( "game,label", [(gbt.Game.new_table([2, 2]), "outcome label")] diff --git a/tests/test_players.py b/tests/test_players.py index 9247f524b4..bcc10b0267 100644 --- a/tests/test_players.py +++ b/tests/test_players.py @@ -10,13 +10,29 @@ def test_player_count(): assert len(game.players) == 2 -def test_player_label(): +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_player_label(label): game = gbt.Game.new_table([2, 2]) - pl1, pl2 = game.players - pl1.label = "Alphonse" - pl2.label = "Gaston" - assert pl1.label == "Alphonse" - assert pl2.label == "Gaston" + player = next(iter(game.players)) + player.label = label + assert player.label == label + + +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_player_label_invalid_raises_valueerror(label): + game = gbt.Game.new_table([2, 2]) + player = next(iter(game.players)) + with pytest.raises(ValueError): + player.label = label + + +@pytest.mark.parametrize("label", games.NON_ASCII_LABELS) +def test_player_label_non_ascii_rejected(label): + """ASCII-only for 16.7 (#944); Unicode deferred to #862 (17.0).""" + game = gbt.Game.new_table([2, 2]) + player = next(iter(game.players)) + with pytest.raises(UnicodeEncodeError): + player.label = label def test_player_index_by_string(): @@ -115,6 +131,20 @@ def test_player_strategy_by_label(): assert pl1.strategies["Cooperate"].label == "Cooperate" +@pytest.mark.parametrize("label", games.VALID_LABELS) +def test_add_strategy_label_valid(label): + game = gbt.Game.new_table([2, 2]) + strategy = game.add_strategy(next(iter(game.players)), label) + assert strategy.label == label + + +@pytest.mark.parametrize("label", games.INVALID_LABELS) +def test_add_strategy_label_invalid_raises_valueerror(label): + game = gbt.Game.new_table([2, 2]) + with pytest.raises(ValueError): + game.add_strategy(next(iter(game.players)), label) + + def test_player_strategy_bad_label(): game = gbt.Game.new_table([2, 2]) pl1 = next(iter(game.players)) From fa99ff9e4222344775e9e9719a74a363b1e06bd6 Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 22 Jun 2026 12:43:42 +0100 Subject: [PATCH 4/6] Document label validation --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 08a0ee71ae..3b8b1aa7b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -39,6 +39,8 @@ - Payoff editing in extensive games in the graphical interface is now done via a context popup window rather than text controls drawn (not always well!) over the game tree display. (#947) - In `pygambit`, indexing game object collections by integer position has been removed. (#942) +- Validity of game object labels is enforced (printable ASCII and spaces only, no leading/trailing or + double spaces); invalid labels raise `ValueError` in `pygambit`. (#944) ### Removed - Built-in plotting of logit QRE for strategic games has been removed in the GUI (#809) From f5d9d6262db51353ce57aaf93c8dde3073eadd2b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 25 Jun 2026 13:10:29 +0100 Subject: [PATCH 5/6] Guard exceptions when setting strategy labels in normal form table in GUI --- src/gui/nfgtable.cc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index 74df50984d..cb5d7cafa0 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -1332,7 +1332,12 @@ void TableWidget::RenameRowHeaderStrategy(int headerCol, int headerRow, const wx const int player = GetRowHeaderPlayer(headerCol); const int strat = GetRowHeaderStrategy(headerCol, headerRow); - m_doc->DoSetStrategyLabel(GetStrategyByPlayerAndIndex(player, strat), value); + try { + m_doc->DoSetStrategyLabel(GetStrategyByPlayerAndIndex(player, strat), value); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } void TableWidget::RenameColHeaderStrategy(int headerRow, int headerCol, const wxString &value) @@ -1340,7 +1345,12 @@ void TableWidget::RenameColHeaderStrategy(int headerRow, int headerCol, const wx const int player = GetColHeaderPlayer(headerRow); const int strat = GetColHeaderStrategy(headerRow, headerCol); - m_doc->DoSetStrategyLabel(GetStrategyByPlayerAndIndex(player, strat), value); + try { + m_doc->DoSetStrategyLabel(GetStrategyByPlayerAndIndex(player, strat), value); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } void TableWidget::DeleteRowHeaderStrategy(int headerCol, int headerRow) From 4abd0117755fc4998bd49b7bc8f677381e2476a0 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 25 Jun 2026 13:15:47 +0100 Subject: [PATCH 6/6] Guard valid label exception in GUI for players --- src/gui/efgpanel.cc | 16 ++++++++++++++-- src/gui/nfgpanel.cc | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/gui/efgpanel.cc b/src/gui/efgpanel.cc index a58950f2e5..a552ab8aff 100644 --- a/src/gui/efgpanel.cc +++ b/src/gui/efgpanel.cc @@ -32,6 +32,8 @@ #include // for SVG output #include "efgpanel.h" + +#include "dlexcept.h" #include "efgdisplay.h" // FIXME: communicate with tree window via events. #include "menuconst.h" #include "edittext.h" @@ -282,14 +284,24 @@ void gbtTreePlayerPanel::OnEditPlayerLabel(wxCommandEvent &) void gbtTreePlayerPanel::OnAcceptPlayerLabel(wxCommandEvent &) { - m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + try { + m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } void gbtTreePlayerPanel::PostPendingChanges() { if (m_playerLabel->IsEditing()) { m_playerLabel->EndEdit(true); - m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + try { + m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } } diff --git a/src/gui/nfgpanel.cc b/src/gui/nfgpanel.cc index 91de603214..31c06ed89a 100644 --- a/src/gui/nfgpanel.cc +++ b/src/gui/nfgpanel.cc @@ -29,6 +29,8 @@ #include "gamedoc.h" #include "nfgpanel.h" + +#include "dlexcept.h" #include "nfgtable.h" #include "menuconst.h" #include "edittext.h" @@ -219,14 +221,24 @@ void TablePlayerPanel::OnEditPlayerLabel(wxCommandEvent &) void TablePlayerPanel::OnAcceptPlayerLabel(wxCommandEvent &) { - m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + try { + m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } void TablePlayerPanel::PostPendingChanges() { if (m_playerLabel->IsEditing()) { m_playerLabel->EndEdit(true); - m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + try { + m_doc->DoSetPlayerLabel(m_doc->GetGame()->GetPlayer(m_player), m_playerLabel->GetValue()); + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } } }