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
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ Settings-bearing programs are parsed into the canonical WIR settings carrier
and emitted by the library. Locale detection is available via
`workshop_rs::detect`.

### Rule event contract

The public WIR event contract is locale- and provider-independent. It includes
`Event::Global`, `Event::EachPlayer`, filtered `Event::EachPlayerWithFilters`,
nine filtered `Event::Player` identities (`PlayerEventKind`), and
`Event::Subroutine`. Filtered events carry a canonical `EventTeam` and an
`EventTarget` (`All`, Workshop slot `0..=11`, or a canonical hero id).
Raw Workshop player events require both their team and player filters;
parameterless `Ongoing - Each Player` remains supported for existing programs.
Event identities and filter members are checked against the catalog before a
program is accepted for canonical emission, so source-language providers can
consume this public model without defining a second event table.

## CLI usage

```sh
Expand Down Expand Up @@ -105,11 +118,11 @@ the canonical form with a fresh digest (byte-idempotent). See

## Locale status

* `en-US`: complete declared surface (341/341 canonical entries), corpus
* `en-US`: complete declared surface (366/366 canonical entries), corpus
round-trips and settings emission tested.
* `zh-CN`: the reviewed export-backed corpus covers **341/341** canonical
entries (structural 11/11, actions 60/60, values 77/77, events 3/3,
operators 14/14, enum members 176/176). The declared surface is complete;
* `zh-CN`: the reviewed export-backed corpus covers **366/366** canonical
entries (structural 11/11, actions 60/60, values 77/77, events 12/12,
operators 14/14, enum members 192/192). The declared surface is complete;
settings data covers labels 19/19 and all other declared settings sections.

The corpus is reproducible with the user-provided export (not committed):
Expand Down
4 changes: 2 additions & 2 deletions crates/workshop-rs-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ fn locales_lists_declared_locales_with_coverage() {
let stdout = String::from_utf8(output.stdout).unwrap();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].starts_with("en-us 341/341"), "{stdout}");
assert!(lines[1].starts_with("zh-cn 341/341"), "{stdout}");
assert!(lines[0].starts_with("en-us 366/366"), "{stdout}");
assert!(lines[1].starts_with("zh-cn 366/366"), "{stdout}");
}

#[test]
Expand Down
55 changes: 52 additions & 3 deletions crates/workshop-rs/src/bin/workshop-catalog-gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ mod corpus {
}

/// An exact-en-US-spelling index over a slice of the export.
#[derive(Debug, Default)]
#[derive(Debug, Clone, Default)]
struct Index {
by_en: HashMap<String, Vec<Candidate>>,
}
Expand Down Expand Up @@ -303,6 +303,28 @@ mod corpus {
index
}

/// Build an index from a nested `data.<parent>.<section>` table with
/// direct en-US/zh-CN fields.
fn nested_data_index(export: &Value, parent: &str, section: &str) -> Index {
let mut index = Index::default();
let Some(entries) = export
.get("data")
.and_then(|data| data.get(parent))
.and_then(|parent| parent.get(section))
.and_then(Value::as_object)
else {
return index;
};
for (id, entry) in entries {
let en = entry.get("en-US").and_then(Value::as_str);
let zh = entry.get("zh-CN").and_then(Value::as_str);
if let (Some(en), Some(zh)) = (en, zh) {
index.add(&format!("data.{parent}.{section}.{id}"), en, zh);
}
}
index
}

/// Resolve a user-confirmed legacy identity whose export wording differs
/// from the canonical English spelling. The export entry is accepted only
/// when its category, identity, GUID, and both locale values match the
Expand Down Expand Up @@ -565,6 +587,26 @@ mod corpus {
let actions = localized_index(&export, &["actions."]);
let values = localized_index(&export, &["values."]);
let events = localized_index(&export, &["other.events."]);
let event_teams = {
let mut index = localized_index(&export, &["other.eventTeams."]);
merge_index(
&mut index,
nested_data_index(&export, "other", "eventTeams"),
);
index
};
let event_players = {
let mut index = localized_index(&export, &["other.eventPlayers.", "other.eventSlots."]);
merge_index(
&mut index,
nested_data_index(&export, "other", "eventPlayers"),
);
merge_index(
&mut index,
nested_data_index(&export, "other", "eventSlots"),
);
index
};
let operators = localized_index(&export, &["values.", "constants.__Operation__."]);
let maps = {
let mut index = localized_index(&export, &["maps."]);
Expand All @@ -582,6 +624,8 @@ mod corpus {
let mut constants_by_domain: HashMap<String, Index> = HashMap::new();
for domain in enum_domains(&catalog)? {
let export_domain = match domain.as_str() {
"EventTeam" => "__event_team__",
"EventPlayer" => "__event_player__",
"Color" => "ColorLiteral",
"Team" => "TeamLiteral",
"Button" => "ButtonLiteral",
Expand All @@ -592,7 +636,12 @@ mod corpus {
other => other,
};
let prefix = format!("constants.{export_domain}.");
constants_by_domain.insert(domain.to_string(), localized_index(&export, &[&prefix]));
let index = match domain.as_str() {
"EventTeam" => event_teams.clone(),
"EventPlayer" => event_players.clone(),
_ => localized_index(&export, &[&prefix]),
};
constants_by_domain.insert(domain.to_string(), index);
}

let mut matches: Vec<Match> = Vec::new();
Expand Down Expand Up @@ -951,7 +1000,7 @@ mod corpus {
"commitDate": meta.get("commitDate").and_then(Value::as_str).unwrap_or("<unknown>"),
"fetchedAt": meta.get("fetchedAt").and_then(Value::as_str).unwrap_or("<unknown>"),
},
"method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)",
"method": "exact en-US spelling match between the catalog aliases and the export's localized index (actions/values/events/operators/constants/event filters/maps/heroes), plus confirmed legacy identity/GUID mappings for global stop-chasing, force hero/throttle, Set Player Allowed Heroes, and bare comparison-symbol entries; zh-CN is taken from the same export entry; entries without an accepted match, or whose export candidates disagree on zh-CN, are excluded with a recorded reason and keep fail-explicit behavior (ADR-0001 Decision 7)",
"sourceReview": "reviewed: workshop-rs commits its own mapping data; the user-provided JSON is build input only and is not redistributed",
"coverage": Value::Object(coverage_all),
"matches": matches_json,
Expand Down
Loading