xiv_emote_parser/log_message/ast/
condition.rs

1//! Known conditions derived from functions and tags in log messages.
2//! Abstracts actual calls so that full output can be pre-calculated with
3//! specific portions that require player data.
4
5use std::borrow::Cow;
6
7use thiserror::Error;
8
9pub use crate::log_message::types::Gender;
10
11use super::types::{FuncName, Function, IfParam, Obj, Param, Tag, TagName};
12
13/// Abstraction of conditions provided by functions and tags in log messages.
14/// Should only appear as the condition for an if-else.
15#[derive(Debug, Clone, Copy)]
16pub enum Condition {
17    /// if the current player character is the origin of the message
18    /// Equal(ObjectParameter(1),ObjectParameter(2))
19    IsSelfOrigin,
20    /// if the current player character is the target of the message
21    /// Equal(ObjectParameter(1),ObjectParameter(3))
22    IsSelfTarget,
23    /// if the origin of the message's gender is female
24    /// <Sheet(BNpcName,PlayerParameter(7),6)/>
25    IsOriginFemale,
26    /// if the origin of the message's gender is female when not a player(?)
27    /// PlayerParameter(5)
28    IsOriginFemaleNpc,
29    /// if the origin of the message is a player
30    /// PlayerParameter(7)
31    IsOriginPlayer,
32    /// if the target of the message is a player
33    /// PlayerParameter(8)
34    IsTargetPlayer,
35}
36
37/// Abstraction of text with value depending on contextual player data.
38/// Should only appear as the then portion of an if-else or otherwise as text.
39#[derive(Debug, Clone, Copy)]
40pub enum DynamicText {
41    /// the name of the origin of the message when not a player
42    /// ObjectParameter(2)
43    NpcOriginName,
44    /// the name of the target of the message when not a player
45    /// ObjectParameter(3)
46    NpcTargetName,
47    /// the EN name of the origin of the message
48    /// <SheetEn(ObjStr,2,PlayerParameter(7),1,1)/>
49    PlayerOriginNameEn,
50    /// the EN name of the target of the message
51    /// <SheetEn(ObjStr,2,PlayerParameter(8),1,1)/>
52    PlayerTargetNameEn,
53    /// the JP name of the origin of the message
54    /// <Sheet(ObjStr,PlayerParameter(7),0)/>
55    PlayerOriginNameJp,
56    /// the JP name of the target of the message
57    /// <Sheet(ObjStr,PlayerParameter(8),0)/>
58    PlayerTargetNameJp,
59}
60
61pub trait ConditionAnswer {
62    fn as_bool(&self, cond: &Condition) -> bool;
63}
64
65pub trait DynamicTextAnswer {
66    fn as_str(&self, text: &DynamicText) -> Cow<'static, str>;
67}
68
69pub trait Answers: ConditionAnswer + DynamicTextAnswer {}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Character {
73    pub name: Cow<'static, str>,
74    pub gender: Gender,
75    pub is_pc: bool,
76    pub is_self: bool,
77}
78
79impl Character {
80    pub const fn new(name: &'static str, gender: Gender, is_pc: bool, is_self: bool) -> Character {
81        Character {
82            name: Cow::Borrowed(name),
83            gender,
84            is_pc,
85            is_self,
86        }
87    }
88
89    pub fn new_from_string(name: String, gender: Gender, is_pc: bool, is_self: bool) -> Character {
90        Character {
91            name: Cow::from(name),
92            gender,
93            is_pc,
94            is_self,
95        }
96    }
97}
98
99#[derive(Debug, Clone)]
100pub struct LogMessageAnswers {
101    origin_character: Character,
102    target_character: Character,
103}
104
105#[derive(Debug, Clone, Error)]
106pub enum LogMessageAnswersError {
107    #[error("Only one character can be self")]
108    MultipleSelves,
109}
110
111impl LogMessageAnswers {
112    pub fn new(
113        origin_character: Character,
114        target_character: Character,
115    ) -> Result<LogMessageAnswers, LogMessageAnswersError> {
116        if origin_character.is_self
117            && target_character.is_self
118            && origin_character != target_character
119        {
120            Err(LogMessageAnswersError::MultipleSelves)
121        } else {
122            Ok(LogMessageAnswers {
123                origin_character,
124                target_character,
125            })
126        }
127    }
128
129    pub fn origin_character(&self) -> &Character {
130        &self.origin_character
131    }
132
133    pub fn target_character(&self) -> &Character {
134        &self.target_character
135    }
136}
137
138impl ConditionAnswer for LogMessageAnswers {
139    fn as_bool(&self, cond: &Condition) -> bool {
140        match cond {
141            Condition::IsSelfOrigin => self.origin_character.is_self,
142            Condition::IsSelfTarget => self.target_character.is_self,
143            Condition::IsOriginFemale | Condition::IsOriginFemaleNpc => {
144                matches!(self.origin_character.gender, Gender::Female)
145            }
146            Condition::IsOriginPlayer => self.origin_character.is_pc,
147            Condition::IsTargetPlayer => self.target_character.is_pc,
148        }
149    }
150}
151
152impl DynamicTextAnswer for LogMessageAnswers {
153    fn as_str(&self, text: &DynamicText) -> Cow<'static, str> {
154        match text {
155            // afaik names are the same regardless of language
156            // todo add option to append world name
157            DynamicText::NpcOriginName
158            | DynamicText::PlayerOriginNameEn
159            | DynamicText::PlayerOriginNameJp => self.origin_character.name.clone(),
160            DynamicText::NpcTargetName
161            | DynamicText::PlayerTargetNameEn
162            | DynamicText::PlayerTargetNameJp => self.target_character.name.clone(),
163        }
164    }
165}
166
167impl Answers for LogMessageAnswers {}
168
169#[derive(Debug, Clone)]
170pub enum Origin {
171    Function(Function),
172    Tag(Tag),
173}
174
175#[derive(Debug, Clone, Error)]
176#[error("Unknown condition ({0:?})")]
177pub struct ConditionError(Origin);
178
179// in TryFrom impls below, Err only bindings provided for clarity
180
181impl TryFrom<&Function> for Condition {
182    type Error = ConditionError;
183
184    fn try_from(fun: &Function) -> Result<Self, Self::Error> {
185        #[allow(clippy::match_single_binding)]
186        match fun.name {
187            FuncName::Equal => match &fun.params[..] {
188                [Param::Function(Function {
189                    name: FuncName::ObjectParameter,
190                    params: p1,
191                }), Param::Function(Function {
192                    name: FuncName::ObjectParameter,
193                    params: p2,
194                })] if matches!(&p1[..], [Param::Num(1)]) && matches!(&p2[..], [Param::Num(2)]) => {
195                    Ok(Condition::IsSelfOrigin)
196                }
197                [Param::Function(Function {
198                    name: FuncName::ObjectParameter,
199                    params: p1,
200                }), Param::Function(Function {
201                    name: FuncName::ObjectParameter,
202                    params: p2,
203                })] if matches!(&p1[..], [Param::Num(1)]) && matches!(&p2[..], [Param::Num(3)]) => {
204                    Ok(Condition::IsSelfTarget)
205                }
206                _ => Err(ConditionError(Origin::Function(fun.clone()))),
207            },
208            FuncName::ObjectParameter => match &fun.params[..] {
209                _ => Err(ConditionError(Origin::Function(fun.clone()))),
210            },
211            FuncName::PlayerParameter => match &fun.params[..] {
212                [Param::Num(7)] => Ok(Condition::IsOriginPlayer),
213                [Param::Num(8)] => Ok(Condition::IsTargetPlayer),
214                [Param::Num(5)] => Ok(Condition::IsOriginFemaleNpc),
215                _ => Err(ConditionError(Origin::Function(fun.clone()))),
216            },
217        }
218    }
219}
220
221impl TryFrom<&Tag> for Condition {
222    type Error = ConditionError;
223
224    fn try_from(tag: &Tag) -> Result<Self, Self::Error> {
225        #[allow(clippy::match_single_binding)]
226        match tag.name {
227            TagName::Clickable => Err(ConditionError(Origin::Tag(tag.clone()))),
228            TagName::Sheet => match &tag.params[..] {
229                [Param::Obj(Obj::BNpcName), Param::Function(Function {
230                    name: FuncName::PlayerParameter,
231                    params: p1,
232                }), Param::Num(6)]
233                    if matches!(&p1[..], [Param::Num(7)]) =>
234                {
235                    Ok(Condition::IsOriginFemale)
236                }
237                _ => Err(ConditionError(Origin::Tag(tag.clone()))),
238            },
239            TagName::SheetEn => match &tag.params[..] {
240                _ => Err(ConditionError(Origin::Tag(tag.clone()))),
241            },
242        }
243    }
244}
245
246impl TryFrom<&IfParam> for Condition {
247    type Error = ConditionError;
248
249    fn try_from(value: &IfParam) -> Result<Self, Self::Error> {
250        match value {
251            IfParam::Function(f) => Condition::try_from(f),
252            IfParam::Tag(t) => Condition::try_from(t),
253        }
254    }
255}
256
257#[derive(Debug, Clone, Error)]
258#[error("Unknown dynamic text ({0:?})")]
259pub struct DynamicTextError(Origin);
260
261impl TryFrom<Function> for DynamicText {
262    type Error = DynamicTextError;
263
264    fn try_from(fun: Function) -> Result<Self, Self::Error> {
265        match fun.name {
266            #[allow(clippy::match_single_binding)]
267            FuncName::Equal => match &fun.params[..] {
268                _ => Err(DynamicTextError(Origin::Function(fun))),
269            },
270            FuncName::ObjectParameter => match &fun.params[..] {
271                [Param::Num(2)] => Ok(DynamicText::NpcOriginName),
272                [Param::Num(3)] => Ok(DynamicText::NpcTargetName),
273                _ => Err(DynamicTextError(Origin::Function(fun))),
274            },
275            #[allow(clippy::match_single_binding)]
276            FuncName::PlayerParameter => match &fun.params[..] {
277                _ => Err(DynamicTextError(Origin::Function(fun))),
278            },
279        }
280    }
281}
282
283impl TryFrom<Tag> for DynamicText {
284    type Error = DynamicTextError;
285
286    fn try_from(tag: Tag) -> Result<Self, Self::Error> {
287        match tag.name {
288            TagName::Clickable => Err(DynamicTextError(Origin::Tag(tag))),
289            TagName::Sheet => match &tag.params[..] {
290                [Param::Obj(Obj::ObjStr), Param::Function(Function {
291                    name: FuncName::PlayerParameter,
292                    params: p1,
293                }), Param::Num(0)]
294                    if matches!(&p1[..], [Param::Num(7)]) =>
295                {
296                    Ok(DynamicText::PlayerOriginNameJp)
297                }
298                [Param::Obj(Obj::ObjStr), Param::Function(Function {
299                    name: FuncName::PlayerParameter,
300                    params: p1,
301                }), Param::Num(0)]
302                    if matches!(&p1[..], [Param::Num(8)]) =>
303                {
304                    Ok(DynamicText::PlayerTargetNameJp)
305                }
306                _ => Err(DynamicTextError(Origin::Tag(tag))),
307            },
308            TagName::SheetEn => match &tag.params[..] {
309                [Param::Obj(Obj::ObjStr), Param::Num(2), Param::Function(Function {
310                    name: FuncName::PlayerParameter,
311                    params: p2,
312                }), Param::Num(1), Param::Num(1)]
313                // this SheetEn usage seems to only appear in Fist Bump (115) untargeted en
314                | [Param::Obj(Obj::ObjStr), Param::Num(2), Param::Function(Function {
315                    name: FuncName::PlayerParameter,
316                    params: p2,
317                }), Param::Num(2), Param::Num(1)]
318                    if matches!(&p2[..], [Param::Num(7)]) =>
319                {
320                    Ok(DynamicText::PlayerOriginNameEn)
321                }
322                [Param::Obj(Obj::ObjStr), Param::Num(2), Param::Function(Function {
323                    name: FuncName::PlayerParameter,
324                    params: p2,
325                }), Param::Num(1), Param::Num(1)]
326                    if matches!(&p2[..], [Param::Num(8)]) =>
327                {
328                    Ok(DynamicText::PlayerTargetNameEn)
329                }
330                _ => Err(DynamicTextError(Origin::Tag(tag))),
331            },
332        }
333    }
334}