pub(crate) mod adapt;
mod context;
pub mod decision;
pub mod geography;
pub mod host;
mod province;
pub mod widget;
#[cfg(test)]
mod tests;
use crate::model::GamePathBuf;
use std::collections::BTreeMap;
use crate::game::eu4::script::parser::{AstFile, AstStatement, AstValue};
pub use context::{Binding, ScopeUse, ScriptCall, ScriptKind, ScriptSource, Stage};
use context::{ContextWalk, Rewrite, ScriptDefinitions};
pub use decision::{GeneratedDecision, GuiOverrides, RouteContext, SourceMod};
pub use host::{HostRoot, documented_host};
pub use widget::{WidgetNode, WidgetPath, named_widgets};
#[derive(Clone, Copy)]
pub struct GuiSources<'a> {
pub interface: &'a [AstFile],
pub custom_gui: &'a [AstFile],
pub scripted_effects: &'a [ScriptSource],
pub scripted_triggers: &'a [ScriptSource],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostBinding {
pub window: String,
pub root: HostRoot,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FeatureCondition {
pub widget: String,
pub stage: Stage,
pub statements: Vec<AstStatement>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtractedFeature {
pub widget: WidgetPath,
pub definition_path: GamePathBuf,
pub host: HostBinding,
pub conditions: Vec<FeatureCondition>,
pub effect: Vec<AstStatement>,
pub tooltip: Option<String>,
pub calls: Vec<ScriptCall>,
pub scope_uses: Vec<ScopeUse>,
pub unresolved: Vec<String>,
}
impl ExtractedFeature {
pub fn is_complete(&self) -> bool {
self.unresolved.is_empty()
}
pub fn bindings_of(&self, token: &str) -> Vec<&Binding> {
self.scope_uses
.iter()
.filter(|usage| usage.token.eq_ignore_ascii_case(token))
.map(|usage| &usage.binding)
.collect()
}
}
struct CustomGuiDefinition {
kind: String,
path: GamePathBuf,
items: Vec<AstStatement>,
}
pub struct FeatureCatalog {
widgets: Vec<WidgetPath>,
definitions: BTreeMap<String, Vec<CustomGuiDefinition>>,
order: Vec<String>,
scripts: ScriptDefinitions,
}
impl FeatureCatalog {
pub fn new(sources: GuiSources<'_>) -> Self {
let widgets = sources
.interface
.iter()
.flat_map(|file| named_widgets(file.path.as_str(), file))
.collect();
let mut definitions: BTreeMap<String, Vec<CustomGuiDefinition>> = BTreeMap::new();
let mut order = Vec::new();
for file in sources.custom_gui {
for statement in &file.statements {
let AstStatement::Assignment {
key,
value: AstValue::Block { items, .. },
..
} = statement
else {
continue;
};
let Some(name) = widget::scalar(items, "name") else {
continue;
};
if !definitions.contains_key(&name) {
order.push(name.clone());
}
definitions
.entry(name)
.or_default()
.push(CustomGuiDefinition {
kind: key.clone(),
path: file.path.clone(),
items: items.clone(),
});
}
}
Self {
widgets,
definitions,
order,
scripts: ScriptDefinitions::new(sources.scripted_effects, sources.scripted_triggers),
}
}
pub fn actions(&self) -> Vec<(String, Result<ExtractedFeature, String>)> {
self.order
.iter()
.filter(|name| {
self.definitions[*name]
.iter()
.any(|definition| definition.kind == "custom_button")
})
.map(|name| (name.clone(), self.extract(name)))
.collect()
}
pub fn extract(&self, name: &str) -> Result<ExtractedFeature, String> {
let definition = self.single_definition(name)?;
if definition.kind != "custom_button" {
return Err(format!(
"`{name}` is a `{}`; only `custom_button` actions are extracted",
definition.kind
));
}
let widget = self.single_widget(name)?;
let (window, root) = widget
.ancestry
.iter()
.rev()
.find_map(|node| {
documented_host(&widget.gui_file, &node.name).map(|root| (node.name.clone(), root))
})
.ok_or_else(|| {
format!(
"`{name}` in `{}` is not under a documented host window",
widget.gui_file
)
})?;
let mut unresolved = Vec::new();
let mut conditions = Vec::new();
for ancestor in &widget.ancestry[..widget.ancestry.len() - 1] {
if !ancestor.scripted {
continue;
}
match self.single_definition(&ancestor.name) {
Ok(parent) => {
if let Some(potential) = block(&parent.items, "potential") {
conditions.push(FeatureCondition {
widget: ancestor.name.clone(),
stage: Stage::Potential,
statements: potential,
});
}
}
Err(reason) => unresolved.push(format!("scripted ancestor: {reason}")),
}
}
for (key, stage) in [("potential", Stage::Potential), ("trigger", Stage::Trigger)] {
if let Some(statements) = block(&definition.items, key) {
conditions.push(FeatureCondition {
widget: name.to_owned(),
stage,
statements,
});
}
}
let effect = block(&definition.items, "effect").unwrap_or_default();
let root_binding = match root {
HostRoot::Actor => Binding::Actor,
other => Binding::Target(other),
};
let mut walk = ContextWalk::new(&self.scripts, root_binding);
for condition in &conditions {
walk.walk(condition.stage, &condition.widget, &condition.statements);
}
walk.walk(Stage::Effect, name, &effect);
unresolved.extend(walk.unresolved);
Ok(ExtractedFeature {
widget: widget.clone(),
definition_path: definition.path.clone(),
host: HostBinding { window, root },
conditions,
effect,
tooltip: widget::scalar(&definition.items, "tooltip"),
calls: walk.calls,
scope_uses: walk.uses,
unresolved,
})
}
fn rewrite<'a>(&'a self, target: Option<&'a str>) -> Rewrite<'a> {
Rewrite {
definitions: &self.scripts,
target,
texts: None,
instances: &[],
}
}
fn rewrite_text<'a>(
&'a self,
target: Option<&'a str>,
instances: &'a [String],
texts: &'a context::Localise<'a>,
) -> Rewrite<'a> {
Rewrite {
definitions: &self.scripts,
target,
texts: Some(texts),
instances,
}
}
fn single_definition(&self, name: &str) -> Result<&CustomGuiDefinition, String> {
match self.definitions.get(name).map(Vec::as_slice) {
Some([definition]) => Ok(definition),
Some(many) => Err(format!(
"`{name}` has {} custom_gui definitions: {}",
many.len(),
many.iter()
.map(|definition| definition.path.to_string())
.collect::<Vec<_>>()
.join(", ")
)),
None => Err(format!("`{name}` has no custom_gui definition")),
}
}
fn single_widget(&self, name: &str) -> Result<&WidgetPath, String> {
let matches = self
.widgets
.iter()
.filter(|path| path.leaf().scripted && path.leaf().name == name)
.collect::<Vec<_>>();
match matches.as_slice() {
[widget] => Ok(widget),
[] => Err(format!("`{name}` has no scripted widget")),
many => Err(format!(
"`{name}` matches {} scripted widgets: {}",
many.len(),
many.iter()
.map(|path| path.gui_file.as_str())
.collect::<Vec<_>>()
.join(", ")
)),
}
}
}
fn block(items: &[AstStatement], key: &str) -> Option<Vec<AstStatement>> {
items.iter().find_map(|statement| match statement {
AstStatement::Assignment {
key: found,
value: AstValue::Block { items, .. },
..
} if found == key => Some(items.clone()),
_ => None,
})
}