Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 15 additions & 9 deletions crates/workshop-rs/src/values/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,15 +734,21 @@ impl ParseContext<'_> {
Some(Span::new(self.file(), start, end)),
)));
}
if let Some(expected) = self.expected_domain {
if let Some((value_type, value)) = self.resolve_enum_member_mixed(expected, phrase) {
return Ok(self.target.values.push(ValueNode::new(
Value::Enum { value_type, value },
Some(Span::new(self.file(), start, end)),
)));
}
}
if let Some((value_type, value)) = self.resolve_enum_member_mixed("Team", phrase) {
let member = self
.expected_domain
.filter(|domain| *domain == "Color")
.and_then(|_| self.resolve_enum_member_mixed("Team", phrase))
.filter(|(_, member)| {
self.catalog
.enum_spelling("Color", self.catalog.primary_locale(), member)
.is_some()
})
.or_else(|| {
self.expected_domain
.and_then(|domain| self.resolve_enum_member_mixed(domain, phrase))
})
.or_else(|| self.resolve_enum_member_mixed("Team", phrase));
if let Some((value_type, value)) = member {
return Ok(self.target.values.push(ValueNode::new(
Value::Enum { value_type, value },
Some(Span::new(self.file(), start, end)),
Expand Down
52 changes: 38 additions & 14 deletions crates/workshop-rs/src/values/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,7 @@ pub(crate) fn validate_call_signature(
let valid = match &node.value {
wir::Value::Enum {
value_type, value, ..
} => {
value_type == domain
&& catalog
.enum_spelling(domain, catalog.primary_locale(), value)
.is_some()
}
} => enum_member_matches_domain(catalog, value_type, value, domain),
wir::Value::Call { name, .. } if name == wir::AMBIGUOUS_ENUM_CALL => {
wir::ambiguous_enum_parts(program, *arg_id).is_some_and(|(_, candidate_ids)| {
candidate_ids.iter().any(|candidate_id| {
Expand All @@ -271,10 +266,7 @@ pub(crate) fn validate_call_signature(
Some(wir::ValueNode {
value: wir::Value::Enum { value_type, value },
..
}) if value_type == domain
&& catalog
.enum_spelling(domain, catalog.primary_locale(), value)
.is_some()
}) if enum_member_matches_domain(catalog, value_type, value, domain)
)
})
})
Expand Down Expand Up @@ -395,9 +387,14 @@ fn value_matches_type(
matches!(
program.values.get(*candidate_id),
Some(wir::ValueNode {
value: wir::Value::Enum { value_type, .. },
value: wir::Value::Enum { value_type, value },
..
}) if value_type == alternative
}) if enum_type_matches_domain(
catalog,
value_type,
value,
alternative,
)
)
})
},
Expand Down Expand Up @@ -426,8 +423,9 @@ fn value_matches_single_type(catalog: &Catalog, value: &wir::Value, expected: &s
| wir::Value::Vector { .. },
"Object",
) => true,
(wir::Value::Enum { value_type, .. }, domain) => {
matches!(domain, "Any" | "Unknown" | "Object") || value_type == domain
(wir::Value::Enum { value_type, value }, domain) => {
matches!(domain, "Any" | "Unknown" | "Object")
|| enum_type_matches_domain(catalog, value_type, value, domain)
}
(wir::Value::Call { name, .. }, expected) => {
if expected == "Operation"
Expand Down Expand Up @@ -483,6 +481,32 @@ fn value_matches_single_type(catalog: &Catalog, value: &wir::Value, expected: &s
}
}

fn enum_type_matches_domain(
catalog: &Catalog,
value_type: &str,
value: &str,
expected_domain: &str,
) -> bool {
value_type == expected_domain
|| (value_type == "Team"
&& expected_domain == "Color"
&& catalog
.enum_spelling(expected_domain, catalog.primary_locale(), value)
.is_some())
}

fn enum_member_matches_domain(
catalog: &Catalog,
value_type: &str,
value: &str,
expected_domain: &str,
) -> bool {
enum_type_matches_domain(catalog, value_type, value, expected_domain)
&& catalog
.enum_spelling(expected_domain, catalog.primary_locale(), value)
.is_some()
}

fn semantic_types_compatible(actual: &str, expected: &str) -> bool {
matches!(actual, "Any" | "Unknown")
|| matches!(expected, "Any" | "Unknown")
Expand Down
27 changes: 27 additions & 0 deletions crates/workshop-rs/tests/contextual_semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,33 @@ fn create_dummy_bot_accepts_hero_or_hero_array_only_for_hero_parameter() {
assert!(error.to_string().contains("semantic type"));
}

#[test]
fn team_members_shared_with_color_are_valid_in_color_slots() {
let accepted = program(
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Team 2, Event Player, 200);",
);
validate_program(&accepted);
assert!(matches!(
&accepted
.values
.get(first_action_args(&accepted)[2])
.expect("play effect color")
.value,
Value::Enum { value_type, value } if value_type == "Team" && value == "TEAM_2"
));

let unsupported = program(
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, All Teams, Event Player, 200);",
);
let error = validate::validate_canonical_ids_wir(&unsupported, &catalog())
.expect_err("a Team member absent from Color must still be rejected in a Color slot");
assert!(
error
.to_string()
.contains("semantic type 'Color', got Team")
);
}

#[test]
fn nested_numeric_boolean_aliases_are_preserved_inside_vector_components() {
let program = program("Set Global Variable(probe, Vector(1, True, False));");
Expand Down
44 changes: 37 additions & 7 deletions crates/workshop-rs/tests/wrapper_forms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ fn a_team_wrapper_in_the_input_is_written_bare_in_every_locale() {
}
}

#[test]
fn team_constants_in_color_slots_keep_their_authored_form() {
for (locale, section, event, actions) in [
(
"en-US",
"rule",
"event",
&[
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Team 1, Event Player, 200);",
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Team 2, Event Player, 200);",
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Color(Team 2), Event Player, 200);",
][..],
),
(
"zh-CN",
"规则",
"事件",
&[
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 队伍1, 事件玩家, 200);",
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 队伍2, 事件玩家, 200);",
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 颜色(队伍2), 事件玩家, 200);",
][..],
),
] {
assert_lines_preserved(actions, locale, event, section);
}
}

#[test]
fn yes_no_settings_are_written_with_the_client_word() {
for (locale, text) in [
Expand All @@ -111,13 +139,15 @@ fn yes_no_settings_are_written_with_the_client_word() {
}

fn wrapper_counts(text: &str) -> Vec<usize> {
["Team", "队伍", "Hero", "英雄", "Button", "按钮"]
.iter()
.map(|name| {
let pattern = format!(r"(^|[^\p{{L}}\p{{N}}_]){name}\(");
regex::Regex::new(&pattern).unwrap().find_iter(text).count()
})
.collect()
[
"Team", "队伍", "Hero", "英雄", "Button", "按钮", "Color", "颜色",
]
.iter()
.map(|name| {
let pattern = format!(r"(^|[^\p{{L}}\p{{N}}_]){name}\(");
regex::Regex::new(&pattern).unwrap().find_iter(text).count()
})
.collect()
}

fn tokens(text: &str) -> BTreeSet<String> {
Expand Down
11 changes: 8 additions & 3 deletions docs/wrapper-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ elements, comparisons, and generic parameters alike. `Team(Team 1)` is never
written. `Game Mode(...)` is a value function in the catalog, not a constant
wrapper, and is preserved as parsed.

`Team 1` and `Team 2` are also Color members. In a Color-typed slot, their bare
spelling resolves to the Team domain and stays bare; `Color(Team 2)` explicitly
selects the Color member and stays wrapped. This preserves the domain expressed
by the authored form when a slot accepts a value from another enum domain.

Evidence: pinned OverPy 9.7.10 compiled a probe covering each domain in
Team-, Hero-, Array-, `Compare`-, and `Custom String`-argument positions, in
en-US and zh-CN, and always wrote the forms above. The real-project fixtures
generated by OverPy keep their `Hero(`/`Button(` call counts through parse and
emit (`tests/wrapper_forms.rs`). Localized wrapper names are the domain aliases
in the catalog (`颜色`, `英雄`, `按钮`, `地图`).
generated by OverPy keep their `Team(`/`Hero(`/`Button(`/`Color(` call counts
through parse and emit (`tests/wrapper_forms.rs`). Localized wrapper names are
the domain aliases in the catalog (`颜色`, `英雄`, `按钮`, `地图`).

## Boolean settings

Expand Down
Loading