Initialize repository for MUDMOUTH
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
use chrono::{DateTime, Local, NaiveDateTime};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LogEvent {
|
||||
NpcDialogue {
|
||||
at: DateTime<Local>,
|
||||
speaker: String,
|
||||
text: String,
|
||||
},
|
||||
ZoneTransition {
|
||||
at: DateTime<Local>,
|
||||
zone: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl LogEvent {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NpcDialogue { .. } => "NPC",
|
||||
Self::ZoneTransition { .. } => "ZONE",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
match self {
|
||||
Self::NpcDialogue { text, .. } => text,
|
||||
Self::ZoneTransition { zone, .. } => zone,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn occurred_at(&self) -> DateTime<Local> {
|
||||
match self {
|
||||
Self::NpcDialogue { at, .. } | Self::ZoneTransition { at, .. } => *at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_line(line: &str) -> Option<LogEvent> {
|
||||
let (timestamp, message) = split_timestamp(line)?;
|
||||
|
||||
if let Some((speaker, text)) = parse_npc_says(message) {
|
||||
return Some(LogEvent::NpcDialogue {
|
||||
at: timestamp,
|
||||
speaker,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
message
|
||||
.strip_prefix("You have entered ")
|
||||
.and_then(|zone| zone.strip_suffix('.'))
|
||||
.filter(|zone| !zone.is_empty())
|
||||
.map(|zone| LogEvent::ZoneTransition {
|
||||
at: timestamp,
|
||||
zone: zone.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn split_timestamp(line: &str) -> Option<(DateTime<Local>, &str)> {
|
||||
let end = line.find("] ")?;
|
||||
let raw_timestamp = line.strip_prefix('[')?.get(..end - 1)?;
|
||||
let timestamp = NaiveDateTime::parse_from_str(raw_timestamp, "%a %b %d %H:%M:%S %Y")
|
||||
.ok()?
|
||||
.and_local_timezone(Local)
|
||||
.single()?;
|
||||
Some((timestamp, &line[end + 2..]))
|
||||
}
|
||||
|
||||
fn parse_npc_says(message: &str) -> Option<(String, String)> {
|
||||
let (speaker, quote) = message.split_once(" says, '")?;
|
||||
let text = quote.strip_suffix('\'')?;
|
||||
if speaker.is_empty() || text.is_empty() || speaker == "You" {
|
||||
return None;
|
||||
}
|
||||
Some((speaker.to_string(), text.to_string()))
|
||||
}
|
||||
|
||||
pub fn speaker_key(name: &str) -> String {
|
||||
let normalized = name
|
||||
.chars()
|
||||
.flat_map(char::to_lowercase)
|
||||
.map(|character| {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
character
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
let normalized = normalized.trim_matches('_');
|
||||
let compact = normalized
|
||||
.split('_')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_");
|
||||
format!("npc:{compact}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_npc_dialogue_with_apostrophe() {
|
||||
let event =
|
||||
parse_line("[Fri Jul 17 20:10:29 2026] Turga says, 'I've got a Rusty Long Sword.'")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
event,
|
||||
LogEvent::NpcDialogue {
|
||||
at: NaiveDateTime::parse_from_str(
|
||||
"Fri Jul 17 20:10:29 2026",
|
||||
"%a %b %d %H:%M:%S %Y"
|
||||
)
|
||||
.unwrap()
|
||||
.and_local_timezone(Local)
|
||||
.single()
|
||||
.unwrap(),
|
||||
speaker: "Turga".into(),
|
||||
text: "I've got a Rusty Long Sword.".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_player_dialogue_and_channel_tells() {
|
||||
assert!(parse_line("[Fri Jul 17 20:10:29 2026] You say, 'Hail'").is_none());
|
||||
assert!(parse_line("[Fri Jul 17 20:10:29 2026] Rath tells General:2, 'hello'").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_zone_message() {
|
||||
let event =
|
||||
parse_line("[Fri Jul 17 20:09:32 2026] You have entered Lake Rathetear.").unwrap();
|
||||
assert!(matches!(event, LogEvent::ZoneTransition { zone, .. } if zone == "Lake Rathetear"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_speaker_keys() {
|
||||
assert_eq!(speaker_key("A lizardman healer"), "npc:a_lizardman_healer");
|
||||
assert_eq!(speaker_key("Zok Caropni!"), "npc:zok_caropni");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user