Skip to content

feat: support switch-case replacements and definitions at root level#1459

Merged
Pablete1234 merged 7 commits intoPGMDev:devfrom
TTtie:feat/switch-case-replacements
Jul 7, 2025
Merged

feat: support switch-case replacements and definitions at root level#1459
Pablete1234 merged 7 commits intoPGMDev:devfrom
TTtie:feat/switch-case-replacements

Conversation

@TTtie
Copy link
Copy Markdown
Contributor

@TTtie TTtie commented Dec 15, 2024

Adds support for switch-case style replacement in message actions, inspired by pattern matching found in modern programming languages, for example (preliminary syntax, some details may be subject to change):

<message text="The outcome is {outcome}">
    <replacements>
        <!-- 
             value supports formulas and may be omitted 
             (in this case, the match attribute in <case>
             is disallowed)
        -->
        <switch id="outcome" value="variable">
            <!-- matches if the formula result is 0 -->
            <case match="0" result="Outcome 1"/>
            
            <!-- matches if the formula result is 1 AND only-team-1 allows -->
            <case match="1" filter="only-team-1" result="`aOutcome 2"/>
            
            <!-- matches if only-team-2 allows regardless of formula result -->
            <case filter="only-team-2" result="`bOutcome 3"/>
            
            <!--
                full syntax (matches if the formula result is between 2 and 5,
                and the inner filter allows)
            -->
            <case>
                <match>2..5</match>
                <filter>
                    <all id="not-moving">
                        <variable var="player.vel_x">0</variable>
                        <variable var="player.vel_y">..0</variable>
                        <variable var="player.vel_z">0</variable>
                    </all>
                </filter>
                <result>`cOutcome 4</result>
            </case>
            
            <!-- a fallback value if no cases match (defaults to an empty component) -->
            <fallback>`dA fallback outcome</fallback>
        </switch>
    </replacements>
</message>

Additionally, replacements can now be specified at the root level:

<map>
    <replacements>
        <!--
            scope is required for all replacements except <player />
            and it will be enforced if specified
        -->
        <decimal id="decimal" value="money" scope="player" format="0.00"/>
        <switch id="team" scope="team">
            <case filter="only-team-1" result="`aLime"/>
        </switch>
        
        <!-- 
            This will error as only-team-1 is team-scoped
            Not true as of commit 54666274 anymore. `only-team-1` will match as it abstains.
        -->
        <switch id="team2" scope="match">
            <case filter="only-team-1" result="`aLime"/>
        </switch>
    </replacements>
    <actions>
        <action id="action" scope="match">
            <switch-scope inner="player">
                <message text="The variable is {variable} and your team is {team}!">
                    <replacements>
                        <!--
                            the syntax for replacement references is
                            <replacement id="local-id">global-id</replacement>
                        -->
                        <replacement id="variable">decimal</replacement>
                        <replacement id="team">team</replacement>
                    </replacements>
                </message>
            </switch-scope>
        </action>
    </actions>
</map>

Launch checklist:

  • Cannot use higher-scope filters in <case> than defined in <switch>
  • Cannot specify higher-scope replacements than defined in <action>
  • Cannot use lower-scope variables in <switch> value
  • Validation???
    • Using a reference of lower scope replacement should not work in a higher-scope action
    • Declaring a lower scope replacement in a higher scope action should not work?

@TTtie TTtie force-pushed the feat/switch-case-replacements branch 6 times, most recently from e8cd0a1 to 6135522 Compare December 16, 2024 00:13
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 6135522 to 64dc8f7 Compare December 19, 2024 21:52
@TTtie TTtie requested a review from Pablete1234 December 19, 2024 21:55
@TTtie TTtie force-pushed the feat/switch-case-replacements branch 6 times, most recently from d2c9388 to 134a7b3 Compare December 22, 2024 11:46
Pablete1234
Pablete1234 previously approved these changes Dec 22, 2024
@TTtie TTtie marked this pull request as ready for review December 22, 2024 15:45
@TTtie TTtie requested a review from cswhite2000 as a code owner December 22, 2024 15:45
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 134a7b3 to b9c28c2 Compare December 22, 2024 15:53
@TTtie TTtie marked this pull request as draft December 22, 2024 15:53
@TTtie TTtie force-pushed the feat/switch-case-replacements branch 2 times, most recently from a8c582a to 7ed2058 Compare December 29, 2024 12:55
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 7ed2058 to ac17739 Compare January 4, 2025 14:24
@TTtie TTtie force-pushed the feat/switch-case-replacements branch 2 times, most recently from e97f48a to eeb41ed Compare January 17, 2025 22:43
@TTtie TTtie force-pushed the feat/switch-case-replacements branch 2 times, most recently from c62a5c5 to eeebf4e Compare February 8, 2025 17:29
@TTtie TTtie force-pushed the feat/switch-case-replacements branch 2 times, most recently from c1d58cd to 2f38722 Compare April 13, 2025 02:02
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 2f38722 to 9c2ae5d Compare April 13, 2025 02:07
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 9c2ae5d to c7187eb Compare June 3, 2025 00:42
@calcastor
Copy link
Copy Markdown
Contributor

Testing with changes from this repo, with the exception of Grand Dueling and Chromatic Conquest which differ too much upstream for me to rebase the changes right now, everything seems to work as intended.

@TTtie TTtie force-pushed the feat/switch-case-replacements branch 2 times, most recently from fd4d849 to 27648ed Compare June 6, 2025 00:10
@TTtie TTtie marked this pull request as ready for review June 25, 2025 01:04
TTtie added 3 commits July 3, 2025 23:30
Adds support for switch-case style replacement in message actions, inspired by pattern matching found in modern programming languages, for example

```xml
<action filter="game=1"><message text="`3Up Next: `b[1] Hungry Reindeer"/></action>
<action filter="game=2"><message text="`3Up Next: `b[2] Present Hunt"/></action>
<action filter="game=3"><message text="`3Up Next: `b[3] Winter Speedway"/></action>
<action filter="game=4"><message text="`3Up Next: `b[4] Parkour"/></action>
<action filter="game=5"><message text="`3Up Next: `b[5] Chimney Sweepers"/></action>
<action filter="game=6"><message text="`3Up Next: `b[6] Frosted Corridors"/></action>
```
becomes
```xml
<message text="`3Up Next: `b[{game}] {game-name}">
    <replacements>
        <decimal id="game" value="game"/>
        <switch id="game-name" value="game">
            <case value="1" result="Hungry Reindeer"/>
            <case value="2" result="Present Hunt"/>
            <case value="3" result="Winter Speedway"/>
            <case value="4" result="Parkour"/>
            <case value="5" result="Chimney Sweepers"/>
            <case value="6" result="Frosted Corridors"/>
        </switch>
    </replacements>
</message>
```

It is also possible to use them with filters:
```xml
<message text="The bomb has been planted on {bombsite} by {holder}">
    <replacements>
        <player id="holder" var="bomb_holder" fallback="an unknown player"/>
        <switch id="bombsite">
            <case filter="planted-a" result="`c${bombsite-a-name}"/>
            <case filter="planted-b" result="`c${bombsite-b-name}"/>
        </switch>
    </replacements>
</message>
```

and also to mix them together:
```xml
<message text="Your score is {score} ({score_num})!">
    <replacements>
        <decimal id="score_num" value="score"/>
        <switch id="score" value="score">
            <case value="0..70" filter="only-attackers" result="`cvery low :("/>
            <case value="0..70" filter="only-defenders" result="`9very low :(" />
            <case value="70..100" filter="only-attackers" result="`clow :|"/>
            <case value="70..100" filter="only-defenders" result="`9low :|" />
            <case value="100..130" filter="only-attackers" result="`chigh :)"/>
            <case value="100..130" filter="only-defenders" result="`9high :)" />
            <fallback>off the charts :O</fallback>
        </switch>
    </replacements>
</message>
```

Signed-off-by: TTtie <me@tttie.cz>
…when parsing inline replacement

Logical correctness is not guaranteed.

Signed-off-by: TTtie <me@tttie.cz>
Signed-off-by: TTtie <me@tttie.cz>
@TTtie TTtie force-pushed the feat/switch-case-replacements branch from 899e7bc to 1a2a9b8 Compare July 3, 2025 23:30
TTtie added 2 commits July 7, 2025 00:45
Instead of throwing a parsing error, the parser will now emit an unused node warning.

Signed-off-by: TTtie <me@tttie.cz>
The usage of Filter#respondsTo hints that it is meant to be used with dynamic filters, which are covered by FilterBuilder#dynamic.

This affects the behavior of the switch-case replacement by allowing any filter to be passed. This also means that filters that abstain will match if used in a case branch.

Signed-off-by: TTtie <me@tttie.cz>
Comment on lines +123 to +128
var filter = parser.filter(innerEl, "filter").optional(() -> {
if (valueRange == null)
throw new InvalidXMLException(
"At least a filter or a match attribute must be specified", innerEl);
return StaticFilter.ALLOW;
});
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.

If you don't specify a value, and do specify a match, you could end up with a weird error message:

"Unused node 'match'" alongside an "At least a filter or a match..." which may leave you puzzled if you were a mapdev

maybe an option would be:

Suggested change
var filter = parser.filter(innerEl, "filter").optional(() -> {
if (valueRange == null)
throw new InvalidXMLException(
"At least a filter or a match attribute must be specified", innerEl);
return StaticFilter.ALLOW;
});
var filterParse = parser.filter(innerEl, "filter");
var filter = valueRange == null ? filterParse.required() : filterParse.orAllow();

which will just mark filter as required (without mentioning match at all) if you've not specified match.
not sure if it's really better or worse, maybe the err message should be different depending on if value is specified or not? i really don't know what's better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If you don't specify a value, and do specify a match, you could end up with a weird error message:

"Unused node 'match'" alongside an "At least a filter or a match..." which may leave you puzzled if you were a mapdev

That will never happen. Parsing errors interrupt the whole map loading process, and unused node warnings are the very last step in that process. The error message probably needs a rewrite given that match="..." is ignored when there's no value though.

TTtie added 2 commits July 7, 2025 14:04
…atch is also unspecified

Signed-off-by: TTtie <me@tttie.cz>
Child elements are also allowed, and in that case, the error wouldn't make sense

Signed-off-by: TTtie <me@tttie.cz>
@Pablete1234 Pablete1234 merged commit 66a49ea into PGMDev:dev Jul 7, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants