Skip to main content

sl_chat_log_parser/
system_messages.rs

1//! Types and parsers for system messages in the chat log
2
3use chumsky::IterParser as _;
4use chumsky::Parser;
5use chumsky::prelude::{any, choice, end, just, one_of};
6use chumsky::text::{digits, newline, whitespace};
7use sl_types::utils::{i64_parser, u64_parser, unsigned_f32_parser, usize_parser};
8
9use crate::take_until;
10
11/// represents a Second Life system message
12#[derive(Debug, Clone, PartialEq)]
13pub enum SystemMessage {
14    /// message about a saved snapshot
15    SavedSnapshot {
16        /// the snapshot filename
17        filename: std::path::PathBuf,
18    },
19    /// message about a failure to save a snapshot due to missing destination folder
20    FailedToSaveSnapshotDueToMissingDestinationFolder {
21        /// the snapshot folder
22        folder: std::path::PathBuf,
23    },
24    /// message about a failure to save a snapshot due to disk space
25    FailedToSaveSnapshotDueToDiskSpace {
26        /// the snapshot folder
27        folder: std::path::PathBuf,
28        /// the amount of space required
29        required_disk_space: bytesize::ByteSize,
30        /// the amount of free space reported
31        free_disk_space: bytesize::ByteSize,
32    },
33    /// message about the draw distance being set to a specific value
34    DrawDistanceSet {
35        /// the distance the draw distance was set to
36        distance: sl_types::map::Distance,
37    },
38    /// message about the home position being set
39    HomePositionSet,
40    /// message about land being divided
41    LandDivided,
42    /// message about a failure to join land due to region boundary
43    FailedToJoinLandDueToRegionBoundary,
44    /// message about offering a calling card
45    OfferedCallingCard {
46        /// the name of the avatar we offered a calling card to
47        recipient_avatar_name: String,
48    },
49    /// message about a saved attachment
50    AttachmentSavedMessage,
51    /// message about paying for an object
52    YouPaidForObject {
53        /// the seller avatar or group
54        seller: sl_types::key::OwnerKey,
55        /// the amount paid
56        amount: sl_types::money::LindenAmount,
57        /// the name of the object you paid for
58        object_name: String,
59    },
60    /// message about paying to create a group
61    YouPaidToCreateGroup {
62        /// the agent you paid
63        payment_recipient: sl_types::key::AgentKey,
64        /// the amount you paid
65        amount: sl_types::money::LindenAmount,
66    },
67    /// message about paying to join a group
68    YouPaidToJoinGroup {
69        /// the group key for the joined group
70        joined_group: sl_types::key::GroupKey,
71        /// the amount paid to join
72        join_fee: sl_types::money::LindenAmount,
73    },
74    /// message about paying for a parcel of land
75    YouPaidForLand {
76        /// previous land owner
77        previous_land_owner: sl_types::key::OwnerKey,
78        /// the amount paid
79        amount: sl_types::money::LindenAmount,
80    },
81    /// message about a failed payment
82    FailedToPay {
83        /// payment recipient
84        payment_recipient: sl_types::key::OwnerKey,
85        /// the amount that could not be paid
86        amount: sl_types::money::LindenAmount,
87    },
88    /// message about an object being granted permission to take L$
89    ObjectGrantedPermissionToTakeMoney {
90        /// the name of the object
91        object_name: String,
92        /// the owner of the object
93        owner_name: String,
94        /// the region where the object is located
95        object_region: Option<sl_types::map::RegionName>,
96        /// the coordinates within that region
97        object_location: Option<sl_types::map::RegionCoordinates>,
98    },
99    /// message about a sent payment
100    SentPayment {
101        /// the recipient avatar or group key
102        recipient_key: sl_types::key::OwnerKey,
103        /// the amount sent
104        amount: sl_types::money::LindenAmount,
105        /// an optional message
106        message: Option<String>,
107    },
108    /// message about a received payment
109    ReceivedPayment {
110        /// the sender avatar or group key
111        sender_key: sl_types::key::OwnerKey,
112        /// the amount received
113        amount: sl_types::money::LindenAmount,
114        /// an optional message
115        message: Option<String>,
116    },
117    /// message that you have been added to a group
118    AddedToGroup,
119    /// message that you left a group
120    LeftGroup {
121        /// the name of the group left
122        group_name: String,
123    },
124    /// message that you are unable to invite a user to a group because you
125    /// are not in the group
126    UnableToInviteUserDueToMissingGroupMembership,
127    /// message that you are unable to invite a user to a group because the
128    /// user is in a different limited estate than the group
129    UnableToInviteUserToGroupDueToDifferingLimitedEstate,
130    /// message that loading a notecard failed
131    UnableToLoadNotecard,
132    /// message that loading a gesture failed
133    UnableToLoadGesture {
134        /// name of the gesture that could not be loaded
135        gesture_name: String,
136    },
137    /// message about a song playing on stream
138    NowPlaying {
139        /// the song name
140        song_name: String,
141    },
142    /// message about a completed teleport
143    TeleportCompleted {
144        /// teleported originated at this location
145        origin: sl_types::map::UnconstrainedLocation,
146    },
147    /// message about a region restart of the region that the avatar is in
148    RegionRestart,
149    /// message about an object giving the current avatar an object
150    ObjectGaveObject {
151        /// the giving object name
152        giving_object_name: String,
153        /// the giving object location
154        giving_object_location: sl_types::map::UnconstrainedLocation,
155        /// the giving object owner
156        giving_object_owner: sl_types::key::OwnerKey,
157        /// the name of the given object
158        given_object_name: String,
159    },
160    /// message about an object giving the current avatar a folder
161    ObjectGaveFolder {
162        /// key of the object
163        giving_object_key: sl_types::key::ObjectKey,
164        /// name of the object
165        giving_object_name: String,
166        /// owner of the object
167        giving_object_owner: sl_types::key::OwnerKey,
168        /// object location
169        giving_object_location: sl_types::map::Location,
170        /// giving object link label
171        giving_object_link_label: String,
172        /// given folder name
173        folder_name: String,
174    },
175    /// message about an avatar giving the current avatar an object
176    AvatarGaveObject {
177        /// is the giving avatar a group member
178        is_group_member: bool,
179        /// the giving avatar name
180        giving_avatar_name: String,
181        /// the name of the given object
182        given_object_name: String,
183    },
184    /// message about you declining an object given to you
185    DeclinedGivenObject {
186        /// the name of the declined object
187        object_name: String,
188        /// the location of the giver
189        giver_location: sl_types::map::UnconstrainedLocation,
190        /// the name of the giver
191        giver_name: String,
192    },
193    /// message asking to select residents to share with
194    SelectResidentsToShareWith,
195    /// message about successfully shared items
196    ItemsSuccessfullyShared,
197    /// message about a modified search query
198    ModifiedSearchQuery {
199        /// the modified query
200        query: String,
201    },
202    /// message about different simulator version
203    SimulatorVersion {
204        /// the previous region simulator version
205        previous_region_simulator_version: String,
206        /// the current region simulator version
207        current_region_simulator_version: String,
208    },
209    /// message about a renamed avatar
210    RenamedAvatar {
211        /// the old name
212        old_name: String,
213        /// the new name
214        new_name: String,
215    },
216    /// message about enabling or disabling double-click teleports
217    DoubleClickTeleport {
218        /// whether this event enables or disables double-click teleports
219        enabled: bool,
220    },
221    /// message about enabling or disabling always run
222    AlwaysRun {
223        /// whether this event enables or disables always run
224        enabled: bool,
225    },
226    /// message about being added as an estate manager
227    AddedAsEstateManager,
228    /// message that the bridge creation started
229    CreatingBridge,
230    /// message that the bridge was created
231    BridgeCreated,
232    /// message that the bridge creation is still in progress and another one
233    /// can not be created simultaneously
234    BridgeCreationInProgress,
235    /// message that the bridge failed to attach
236    BridgeFailedToAttach,
237    /// message that the bridge failed to attach because something else is using
238    /// the bridge attachment point
239    BridgeFailedToAttachDueToBridgeAttachmentPointInUse,
240    /// message that the bridge was not created
241    BridgeNotCreated,
242    /// message that the bridge was detached
243    BridgeDetached,
244    /// message that the bridge object was not found and the creation was aborted
245    BridgeObjectNotFoundCantProceedWithCreation,
246    /// failed to place object at specified location, please try again
247    FailedToPlaceObjectAtSpecifiedLocation,
248    /// script count changed
249    ScriptCountChanged {
250        /// script count before
251        previous_script_count: u32,
252        /// script count now
253        current_script_count: u32,
254        /// change
255        change: i32,
256    },
257    /// the chat message to a multi-person chat is still being processed
258    MultiPersonChatMessageStillBeingProcessed,
259    /// the chat message to an im session that no longer exists is still being processed
260    ChatMessageToNoLongerExistingImSessionStillBeingProcessed,
261    /// the chat message to a conference is still being processed
262    ConferenceChatMessageStillBeingProcessed {
263        /// the name of the avatar whose conference it is
264        avatar_name: String,
265    },
266    /// the group chat message is still being processed
267    GroupChatMessageStillBeingProcessed {
268        /// the name of the group
269        group_name: String,
270    },
271    /// avatar has declined voice call
272    AvatarDeclinedVoice {
273        /// the avatar who declined our voice call
274        avatar_name: String,
275    },
276    /// avatar is not available for voice call
277    AvatarUnavailableForVoice {
278        /// the avatar who was unavailable for our voice call
279        avatar_name: String,
280    },
281    /// audio from a specific domain will always be played (on the audio stream)
282    AudioFromDomainWillAlwaysBePlayed {
283        /// the domain whose audio will always be played
284        domain: String,
285    },
286    /// the object is not for sale
287    ObjectNotForSale,
288    /// cannot created requested inventory
289    CanNotCreateRequestedInventory,
290    /// link failed because pieces being too far apart
291    LinkFailedDueToPieceDistance {
292        /// link failed for this many pieces
293        link_failed_pieces: Option<usize>,
294        /// total selected pieces
295        total_selected_pieces: Option<usize>,
296    },
297    /// rezzing an object failed because the parcel is full
298    RezObjectFailedDueToFullParcel {
299        /// name of the object
300        object_name: String,
301        /// name of the parcel
302        parcel_name: String,
303        /// attempted rez location
304        attempted_rez_location: sl_types::map::RegionCoordinates,
305        /// name of the region where the rez failed
306        region_name: sl_types::map::RegionName,
307    },
308    /// creating an object failed because the region is full
309    CreateObjectFailedDueToFullRegion,
310    /// your object has been returned to your inventory Lost and Found folder
311    YourObjectHasBeenReturned {
312        /// name of the returned object
313        object_name: String,
314        /// from parcel name
315        parcel_name: String,
316        /// at location
317        location: sl_types::map::UnconstrainedLocation,
318        /// due to parcel auto return?
319        auto_return: bool,
320    },
321    /// permission to create an object denied
322    PermissionToCreateObjectDenied,
323    /// permission to rez an object denied
324    PermissionToRezObjectDenied {
325        /// name of the object
326        object_name: String,
327        /// name of the parcel
328        parcel_name: String,
329        /// attempted rez location
330        attempted_rez_location: sl_types::map::RegionCoordinates,
331        /// name of the region where the rez failed
332        region_name: sl_types::map::RegionName,
333    },
334    /// permission to reposition an object denied
335    PermissionToRepositionDenied,
336    /// permission to rotate an object denied
337    PermissionToRotateDenied,
338    /// permission to rescale an object denied
339    PermissionToRescaleDenied,
340    /// permission to unlink denied due to missing build permissions on at least one parcel
341    PermissionToUnlinkDeniedDueToMissingParcelBuildPermissions,
342    /// permission to view script denied
343    PermissionToViewScriptDenied,
344    /// permission to view notecard denied
345    PermissionToViewNotecardDenied,
346    /// permission to change shape denied
347    PermissionToChangeShapeDenied,
348    /// permission to enter parcel denied
349    PermissionToEnterParcelDenied,
350    /// permission to enter parcel denied due to ban
351    PermissionToEnterParcelDeniedDueToBan,
352    /// we ejected an avatar
353    EjectedAvatar,
354    /// ejected from parcel
355    EjectedFromParcel,
356    /// no longer allowed and ejected
357    EjectedFromParcelBecauseNoLongerAllowed,
358    /// banned temporarily
359    BannedFromParcelTemporarily {
360        /// How long the ban lasts
361        ban_duration: time::Duration,
362    },
363    /// banned indefinitely
364    BannedFromParcelIndefinitely,
365    /// only group members can visit this area
366    OnlyGroupMembersCanVisitThisArea,
367    /// unable to teleport due to RLV restriction
368    UnableToTeleportDueToRlv,
369    /// unable to open texture due to RLV restriction
370    UnableToOpenTextureDueToRlv,
371    /// unsupported SLurl
372    UnsupportedSlurl,
373    /// SLurl from untrusted browser blocked
374    BlockedUntrustedBrowserSlurl,
375    /// grid status error invalid message format
376    GridStatusErrorInvalidMessageFormat,
377    /// script info object is invalid or out of range
378    ScriptInfoObjectInvalidOrOutOfRange,
379    /// script info
380    ScriptInfo {
381        /// name of the object or avatar whose script info this is
382        name: String,
383        /// running scripts
384        running_scripts: usize,
385        /// total scripts
386        total_scripts: usize,
387        /// allowed memory size limit
388        allowed_memory_size_limit: bytesize::ByteSize,
389        /// CPU time consumed
390        cpu_time_consumed: time::Duration,
391    },
392    /// Firestorm extended script info
393    ExtendedScriptInfo {
394        /// object key
395        object_key: sl_types::key::ObjectKey,
396        /// description of the inspected object
397        description: Option<String>,
398        /// key of the room prim
399        root_prim: sl_types::key::ObjectKey,
400        /// prim count
401        prim_count: usize,
402        /// land impact
403        land_impact: usize,
404        /// number of items in the inspect object's inventory
405        inventory_items: usize,
406        /// velocity
407        velocity: sl_types::lsl::Vector,
408        /// position in the region
409        position: sl_types::map::RegionCoordinates,
410        /// distance from inspecting avatar to position of inspected object
411        position_distance: sl_types::map::Distance,
412        /// rotation of the inspected object as a quaternion
413        rotation: sl_types::lsl::Rotation,
414        /// rotation of the inspected object as a vector of angles in degrees
415        rotation_vector_degrees: sl_types::lsl::Vector,
416        /// angular velocity of the inspected object in radians per second
417        angular_velocity: sl_types::lsl::Vector,
418        /// creator
419        creator: sl_types::key::AgentKey,
420        /// owner
421        owner: Option<sl_types::key::OwnerKey>,
422        /// previous owner
423        previous_owner: Option<sl_types::key::OwnerKey>,
424        /// rezzed by
425        rezzed_by: sl_types::key::AgentKey,
426        /// group
427        group: Option<sl_types::key::GroupKey>,
428        /// creation time
429        creation_time: Option<time::OffsetDateTime>,
430        /// rez time
431        rez_time: Option<time::OffsetDateTime>,
432        /// pathfinding type
433        pathfinding_type: sl_types::pathfinding::PathfindingType,
434        /// attachment point
435        attachment_point: Option<sl_types::attachment::AttachmentPoint>,
436        /// temporarily attached
437        temporarily_attached: bool,
438        /// inspecting avatar position
439        inspecting_avatar_position: sl_types::map::RegionCoordinates,
440    },
441    /// usage instruction for dice roll command
442    DiceRollCommandUsageInstructions,
443    /// dice roll result
444    DiceRollResult {
445        /// which dice roll (when multiple rolls were requested)
446        roll_number: usize,
447        /// how many faces on the dice rolled
448        dice_faces: usize,
449        /// roll result
450        roll_result: usize,
451    },
452    /// dice roll result sum
453    DiceRollResultSum {
454        /// number of rolls
455        roll_count: usize,
456        /// how many faces on the dice rolled
457        dice_faces: usize,
458        /// result sum
459        result_sum: usize,
460    },
461    /// texture info for object (followed by one or more of the below)
462    TextureInfoForObject {
463        /// name of the object
464        object_name: String,
465    },
466    /// texture info for one face
467    TextureInfoForFace {
468        /// number of the face this line is about
469        face_number: usize,
470        /// width of the texture
471        texture_width: u16,
472        /// height of the texture
473        texture_height: u16,
474        /// type of texture, e.g. opaque, alpha
475        texture_type: String,
476    },
477    /// a message from the Firestorm developers
478    FirestormMessage {
479        /// the type of message, basically whatever follows the initial
480        /// Firestorm up until the exclamation mark (e.g. Tip, Help, Classes,...)
481        message_type: String,
482        /// the actual message, everything after the exclamation mark
483        message: String,
484    },
485    /// message about a grid status event
486    GridStatusEvent {
487        /// event title
488        title: String,
489        /// is this a scheduled event
490        scheduled: bool,
491        /// event body
492        body: String,
493        /// event URL
494        incident_url: String,
495    },
496    /// message with a link at the end (mostly announcements of events or
497    /// similar message of the day style stuff)
498    SystemMessageWithLink {
499        /// the message before the link
500        message: String,
501        /// the link
502        link: String,
503    },
504    /// Firestorm holiday wishes
505    FirestormHolidayWishes {
506        /// the message
507        message: String,
508    },
509    /// Warning about phishing
510    PhishingWarning {
511        /// the message
512        message: String,
513    },
514    /// Test MOTD
515    TestMessageOfTheDay,
516    /// Early Firestorm startup message
517    EarlyFirestormStartupMessage {
518        /// the message
519        message: String,
520    },
521    /// other system message
522    OtherSystemMessage {
523        /// the raw message
524        message: String,
525    },
526}
527
528/// parse a system message about a saved snapshot
529///
530/// # Errors
531///
532/// returns an error if the string could not be parsed
533#[must_use]
534pub fn snapshot_saved_message_parser<'src>()
535-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
536{
537    just("Snapshot saved: ")
538        .ignore_then(
539            any()
540                .repeated()
541                .collect::<String>()
542                .map(std::path::PathBuf::from),
543        )
544        .map(|filename| SystemMessage::SavedSnapshot { filename })
545        .or(just("Failed to save snapshot to ").ignore_then(
546            take_until!(just(": Directory does not exist.").ignored()).map(|(folder, ())| {
547                SystemMessage::FailedToSaveSnapshotDueToMissingDestinationFolder {
548                    folder: std::path::PathBuf::from(folder),
549                }
550            }),
551        ))
552        .or(just("Failed to save snapshot to ").ignore_then(
553            take_until!(just(": Disk is full. ").ignored())
554                .map(|(folder, ())| std::path::PathBuf::from(folder))
555                .then(u64_parser())
556                .then_ignore(just("KB is required but only "))
557                .then(u64_parser())
558                .then_ignore(just("KB is free."))
559                .map(|((folder, required), free)| {
560                    let required_disk_space = bytesize::ByteSize::kib(required);
561                    let free_disk_space = bytesize::ByteSize::kib(free);
562                    SystemMessage::FailedToSaveSnapshotDueToDiskSpace {
563                        folder,
564                        required_disk_space,
565                        free_disk_space,
566                    }
567                }),
568        ))
569}
570
571/// parse a system message about the draw distance being set to a specific distance
572///
573/// # Errors
574///
575/// returns an error if the string could not be parsed
576#[must_use]
577pub fn draw_distance_set_message_parser<'src>()
578-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
579{
580    just("Draw Distance set to ")
581        .ignore_then(sl_types::map::distance_parser())
582        .then_ignore(just('.'))
583        .map(|distance| SystemMessage::DrawDistanceSet { distance })
584        .labelled("draw distance set")
585}
586
587/// parse a system message about the home position being set
588///
589/// # Errors
590///
591/// returns an error if the string could not be parsed
592#[must_use]
593pub fn home_position_set_message_parser<'src>()
594-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
595{
596    just("Home position set.")
597        .to(SystemMessage::HomePositionSet)
598        .labelled("home position set")
599}
600
601/// parse a system message about land being divided
602///
603/// # Errors
604///
605/// returns an error if the string could not be parsed
606#[must_use]
607pub fn land_divided_message_parser<'src>()
608-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
609{
610    just("Land has been divided.")
611        .to(SystemMessage::LandDivided)
612        .labelled("land has been divided")
613}
614
615/// parse a system message about a failure to join land due to a region boundary
616///
617/// # Errors
618///
619/// returns an error if the string could not be parsed
620#[must_use]
621pub fn failed_to_join_land_due_to_region_boundary_message_parser<'src>()
622-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
623{
624    just("Selected land is not all in the same region.")
625        .ignore_then(newline())
626        .ignore_then(just(" Try selecting a smaller piece of land."))
627        .to(SystemMessage::FailedToJoinLandDueToRegionBoundary)
628        .labelled("selected labe is not all in the same region")
629}
630
631/// parse a system message about offering a calling card to an avatar
632///
633/// # Errors
634///
635/// returns an error if the string could not be parsed
636#[must_use]
637pub fn offered_calling_card_message_parser<'src>()
638-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
639{
640    just("You have offered a calling card to ")
641        .ignore_then(take_until!(just('.')))
642        .map(
643            |(recipient_avatar_name, _)| SystemMessage::OfferedCallingCard {
644                recipient_avatar_name,
645            },
646        )
647        .labelled("offered calling card")
648}
649
650/// parse a system message about a saved attachment
651///
652/// # Errors
653///
654/// returns an error if the string could not be parsed
655#[must_use]
656pub fn attachment_saved_message_parser<'src>()
657-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
658{
659    just("Attachment has been saved.")
660        .to(SystemMessage::AttachmentSavedMessage)
661        .labelled("attachment has been saved")
662}
663
664/// parse a system message about a sent payment
665///
666/// # Errors
667///
668/// returns an error if the string could not be parsed
669#[must_use]
670pub fn you_paid_for_object_message_parser<'src>()
671-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
672{
673    just("You paid ")
674        .ignore_then(sl_types::key::app_agent_or_group_uri_as_owner_key_parser())
675        .then_ignore(whitespace())
676        .then(sl_types::money::linden_amount_parser())
677        .then_ignore(just(" for "))
678        .then(take_until!(just(".").ignore_then(end())))
679        .map(
680            |((seller, amount), (object_name, ()))| SystemMessage::YouPaidForObject {
681                seller,
682                amount,
683                object_name,
684            },
685        )
686        .labelled("you paid for object")
687}
688
689/// parse a system message about a sent payment
690///
691/// # Errors
692///
693/// returns an error if the string could not be parsed
694#[must_use]
695pub fn sent_payment_message_parser<'src>()
696-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
697{
698    just("You paid ")
699        .ignore_then(
700            sl_types::key::app_agent_or_group_uri_as_owner_key_parser()
701                .then_ignore(whitespace())
702                .then(sl_types::money::linden_amount_parser())
703                .then(
704                    just(": ")
705                        .ignore_then(any().repeated().collect::<String>())
706                        .ignore_then(take_until!(newline().or(end())).map(|(n, ())| Some(n)))
707                        .or(just(".").map(|_| None)),
708                )
709                .map(
710                    |((recipient_key, amount), message)| SystemMessage::SentPayment {
711                        recipient_key,
712                        amount,
713                        message,
714                    },
715                ),
716        )
717        .labelled("you paid avatar")
718}
719
720/// parse a system message about a received payment
721///
722/// # Errors
723///
724/// returns an error if the string could not be parsed
725#[must_use]
726pub fn received_payment_message_parser<'src>()
727-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
728{
729    sl_types::key::app_agent_or_group_uri_as_owner_key_parser()
730        .then_ignore(just(" paid you "))
731        .then(sl_types::money::linden_amount_parser())
732        .then(
733            just(": ")
734                .ignore_then(any().repeated().collect::<String>())
735                .ignore_then(take_until!(newline().or(end())).map(|(n, ())| Some(n)))
736                .or(just(".").map(|_| None)),
737        )
738        .map(
739            |((sender_key, amount), message)| SystemMessage::ReceivedPayment {
740                sender_key,
741                amount,
742                message,
743            },
744        )
745        .labelled("received payment")
746}
747
748/// parse a system message about paying to create a group
749///
750/// # Errors
751///
752/// returns an error if the string could not be parsed
753#[must_use]
754pub fn you_paid_to_create_a_group_message_parser<'src>()
755-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
756{
757    just("You paid ")
758        .ignore_then(sl_types::key::app_agent_uri_as_agent_key_parser())
759        .then_ignore(whitespace())
760        .then(sl_types::money::linden_amount_parser())
761        .then_ignore(just(" to create a group."))
762        .map(
763            |(payment_recipient, amount)| SystemMessage::YouPaidToCreateGroup {
764                payment_recipient,
765                amount,
766            },
767        )
768        .labelled("you paid to create a group")
769}
770
771/// parse a system message about paying to join a group
772///
773/// # Errors
774///
775/// returns an error if the string could not be parsed
776#[must_use]
777pub fn you_paid_to_join_group_message_parser<'src>()
778-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
779{
780    just("You paid ")
781        .ignore_then(sl_types::key::app_group_uri_as_group_key_parser())
782        .then_ignore(whitespace())
783        .then(sl_types::money::linden_amount_parser())
784        .then_ignore(just(" to join a group."))
785        .map(
786            |(joined_group, join_fee)| SystemMessage::YouPaidToJoinGroup {
787                joined_group,
788                join_fee,
789            },
790        )
791        .labelled("you paid to join a group")
792}
793
794/// parse a system message about paying for land
795///
796/// # Errors
797///
798/// returns an error if the string could not be parsed
799#[must_use]
800pub fn you_paid_for_land_message_parser<'src>()
801-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
802{
803    just("You paid ")
804        .ignore_then(sl_types::key::app_agent_or_group_uri_as_owner_key_parser())
805        .then_ignore(whitespace())
806        .then(sl_types::money::linden_amount_parser())
807        .then_ignore(just(" for a parcel of land."))
808        .map(
809            |(previous_land_owner, amount)| SystemMessage::YouPaidForLand {
810                previous_land_owner,
811                amount,
812            },
813        )
814        .labelled("you paid for a parcel of land")
815}
816
817/// parse a system message about a failure to pay
818///
819/// # Errors
820///
821/// returns an error if the string could not be parsed
822#[must_use]
823pub fn failed_to_pay_message_parser<'src>()
824-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
825{
826    just("You failed to pay ")
827        .ignore_then(sl_types::key::app_agent_or_group_uri_as_owner_key_parser())
828        .then_ignore(whitespace())
829        .then(sl_types::money::linden_amount_parser())
830        .then_ignore(just('.'))
831        .map(|(payment_recipient, amount)| SystemMessage::FailedToPay {
832            payment_recipient,
833            amount,
834        })
835        .labelled("you failed to pay")
836}
837
838/// parse a system message about an object being granted permission to take L$
839///
840/// # Errors
841///
842/// returns an error if the string could not be parsed
843#[must_use]
844pub fn object_granted_permission_to_take_money_parser<'src>()
845-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
846{
847    just('\'')
848        .ignore_then(take_until!(just("', an object owned by '")))
849        .then(take_until!(just("', located in ")))
850        .then(
851            just("(unknown region) at ")
852                .to(None)
853                .or(take_until!(just(" at ")).try_map(|(vc, _), span| {
854                    Ok(Some(sl_types::map::RegionName::try_new(vc).map_err(
855                        |err| {
856                            chumsky::error::Rich::custom(
857                                span,
858                                format!("Error creating region name: {err:?}"),
859                            )
860                        },
861                    )?))
862                })),
863        )
864        .then(
865            just("(unknown position)")
866                .to(None)
867                .or(sl_types::utils::f32_parser()
868                    .then_ignore(just(", "))
869                    .then(sl_types::utils::f32_parser())
870                    .then_ignore(just(','))
871                    .then(sl_types::utils::f32_parser())
872                    .map(|((x, y), z)| Some(sl_types::map::RegionCoordinates::new(x, y, z)))),
873        )
874        .then_ignore(just(
875            ", has been granted permission to: Take Linden dollars (L$) from you.",
876        ))
877        .map(
878            |((((object_name, _), (owner_name, _)), object_region), object_location)| {
879                SystemMessage::ObjectGrantedPermissionToTakeMoney {
880                    object_name,
881                    owner_name,
882                    object_region,
883                    object_location,
884                }
885            },
886        )
887        .labelled("object granted permission to take money")
888}
889
890/// parse a system message about being added or leaving a group
891///
892/// # Errors
893///
894/// returns an error if the string could not be parsed
895#[must_use]
896pub fn group_membership_message_parser<'src>()
897-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
898{
899    just("You have been added to the group.")
900        .to(SystemMessage::AddedToGroup)
901        .or(just("You have left the group '")
902            .ignore_then(take_until!(just("'.")))
903            .map(|(group_name, _)| SystemMessage::LeftGroup { group_name }))
904        .labelled("you have been added to the group")
905}
906
907/// parse a system message about the inability to invite a user to a group
908/// you yourself are not a member of
909///
910/// # Errors
911///
912/// returns an error if the string could not be parsed
913#[must_use]
914pub fn unable_to_invite_user_due_to_missing_group_membership_message_parser<'src>()
915-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
916{
917    just("Unable to invite user because you are not in that group.")
918        .to(SystemMessage::UnableToInviteUserDueToMissingGroupMembership)
919        .labelled("unable to invite user because you are not in that group")
920}
921
922/// parse a system message about the inability to invite a user to a group
923/// due to a difference in limited estate between user and group
924///
925/// # Errors
926///
927/// returns an error if the string could not be parsed
928#[must_use]
929pub fn unable_to_invite_user_due_to_differing_limited_estate_message_parser<'src>()
930-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
931{
932    just("Unable to invite users because at least one user is in a different")
933        .ignore_then(newline())
934        .ignore_then(just(" limited estate than the group.").then_ignore(whitespace().then(end())))
935        .to(SystemMessage::UnableToInviteUserToGroupDueToDifferingLimitedEstate)
936        .labelled("unable to invite users because of differing limited estate")
937}
938
939/// parse a system message about the inability to load a notecard
940///
941/// # Errors
942///
943/// returns an error if the string could not be parsed
944#[must_use]
945pub fn unable_to_load_notecard_message_parser<'src>()
946-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
947{
948    just("Unable to load the notecard.")
949        .then_ignore(newline())
950        .then_ignore(whitespace())
951        .then(just("Please try again."))
952        .to(SystemMessage::UnableToLoadNotecard)
953        .labelled("unable to load notecard")
954}
955
956/// parse a system message about the inability to load a gesture
957///
958/// # Errors
959///
960/// returns an error if the string could not be parsed
961#[must_use]
962pub fn unable_to_load_gesture_message_parser<'src>()
963-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
964{
965    just("Unable to load gesture ")
966        .ignore_then(take_until!(just('.').then(end())))
967        .map(|(gesture_name, _)| SystemMessage::UnableToLoadGesture { gesture_name })
968        .labelled("unable to load gesture")
969}
970
971/// parse a system message about a completed teleport
972///
973/// # Errors
974///
975/// returns an error if the string could not be parsed
976#[must_use]
977pub fn teleport_completed_message_parser<'src>()
978-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
979{
980    just("Teleport completed from http://maps.secondlife.com/secondlife/")
981        .ignore_then(sl_types::map::url_unconstrained_location_parser())
982        .try_map(|origin, _span: chumsky::span::SimpleSpan| {
983            Ok(SystemMessage::TeleportCompleted { origin })
984        })
985        .labelled("teleport completed")
986}
987
988/// parse a system message about a now playing song
989///
990/// # Errors
991///
992/// returns an error if the string could not be parsed
993#[must_use]
994pub fn now_playing_message_parser<'src>()
995-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
996{
997    just("Now playing: ")
998        .ignore_then(any().repeated().collect::<String>())
999        .try_map(|song_name, _span: chumsky::span::SimpleSpan| {
1000            Ok(SystemMessage::NowPlaying { song_name })
1001        })
1002        .labelled("now playing")
1003}
1004
1005/// parse a system message about a region restart
1006///
1007/// # Errors
1008///
1009/// returns an error if the string could not be parsed
1010#[must_use]
1011pub fn region_restart_message_parser<'src>()
1012-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1013{
1014    just("The region you are in now is about to restart. If you stay in this region you will be logged out.")
1015    .try_map(|_, _span: chumsky::span::SimpleSpan| {
1016        Ok(SystemMessage::RegionRestart)
1017    })
1018    .labelled("region about to restart")
1019}
1020
1021/// parse a system message about an object giving the current avatar an object
1022///
1023/// # Errors
1024///
1025/// returns an error if the string could not be parsed
1026#[must_use]
1027pub fn object_gave_object_message_parser<'src>()
1028-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1029{
1030    take_until!(just(" owned by "))
1031        .then(sl_types::key::app_agent_or_group_uri_as_owner_key_parser())
1032        .then_ignore(
1033            whitespace()
1034                .or_not()
1035                .ignore_then(just("gave you ").then(just("<nolink>'").or_not())),
1036        )
1037        .then(take_until!(
1038            just("</nolink>'")
1039                .or_not()
1040                .then(whitespace())
1041                .then(just("( http://slurl.com/secondlife/"))
1042        ))
1043        .then(sl_types::map::url_unconstrained_location_parser())
1044        .then_ignore(just(" ).").ignore_then(end()))
1045        .map(
1046            |(
1047                (((giving_object_name, _), giving_object_owner), (given_object_name, _)),
1048                giving_object_location,
1049            )| {
1050                SystemMessage::ObjectGaveObject {
1051                    giving_object_name,
1052                    giving_object_owner,
1053                    given_object_name,
1054                    giving_object_location,
1055                }
1056            },
1057        )
1058        .labelled("object gave you object")
1059}
1060
1061/// parse a system message about an object giving the current avatar a folder
1062///
1063/// # Errors
1064///
1065/// returns an error if the string could not be parsed
1066#[must_use]
1067pub fn object_gave_folder_message_parser<'src>()
1068-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1069{
1070    just("An object named [")
1071        .ignore_then(sl_types::viewer_uri::viewer_app_objectim_uri_parser())
1072        .then_ignore(whitespace())
1073        .then(take_until!(just("] gave you this folder: '")))
1074        .then(take_until!(just('\'').ignore_then(end())))
1075        .try_map(
1076            |((app_objectim_uri, (giving_object_link_label, _)), (folder_name, ())), span| {
1077                match app_objectim_uri {
1078                    sl_types::viewer_uri::ViewerUri::ObjectInstantMessage {
1079                        object_key,
1080                        object_name,
1081                        owner,
1082                        location,
1083                    } => Ok(SystemMessage::ObjectGaveFolder {
1084                        giving_object_key: object_key,
1085                        giving_object_name: object_name,
1086                        giving_object_owner: owner,
1087                        giving_object_location: location,
1088                        giving_object_link_label,
1089                        folder_name,
1090                    }),
1091                    _ => Err(chumsky::error::Rich::custom(
1092                        span,
1093                        "Unexpected type of viewer URI in object gave folder message parser",
1094                    )),
1095                }
1096            },
1097        )
1098        .labelled("object gave you folder")
1099}
1100
1101/// parse a system message about an avatar giving the current avatar an object
1102///
1103/// # Errors
1104///
1105/// returns an error if the string could not be parsed
1106#[must_use]
1107pub fn avatar_gave_object_message_parser<'src>()
1108-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1109{
1110    just("A group member named ")
1111        .or_not()
1112        .then(take_until!(just(" gave you ")))
1113        .then(take_until!(just(".").ignore_then(end())))
1114        .try_map(
1115            |((group_member, (giving_avatar_name, _)), (given_object_name, ())),
1116             _span: chumsky::span::SimpleSpan| {
1117                Ok(SystemMessage::AvatarGaveObject {
1118                    is_group_member: group_member.is_some(),
1119                    giving_avatar_name,
1120                    given_object_name,
1121                })
1122            },
1123        )
1124        .labelled("group member gave you object")
1125}
1126
1127/// parse a system message about declining an object given to you
1128///
1129/// # Errors
1130///
1131/// returns an error if the string could not be parsed
1132#[must_use]
1133pub fn declined_given_object_message_parser<'src>()
1134-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1135{
1136    just("You decline '")
1137        .ignore_then(take_until!(
1138            just("'  ( http://slurl.com/secondlife/").ignored()
1139        ))
1140        .then(sl_types::map::url_unconstrained_location_parser())
1141        .then_ignore(just(" ) from "))
1142        .then(
1143            any()
1144                .repeated()
1145                .collect::<String>()
1146                .map(|s| s.strip_suffix(".").map(|s| s.to_string()).unwrap_or(s)),
1147        )
1148        .map(|(((object_name, ()), giver_location), giver_name)| {
1149            SystemMessage::DeclinedGivenObject {
1150                object_name,
1151                giver_location,
1152                giver_name,
1153            }
1154        })
1155        .labelled("you decline given object")
1156}
1157
1158/// parse a system message asking to select residents to share with
1159///
1160/// # Errors
1161///
1162/// returns an error if the string could not be parsed
1163#[must_use]
1164pub fn select_residents_to_share_with_message_parser<'src>()
1165-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1166{
1167    just("Select residents to share with.")
1168        .to(SystemMessage::SelectResidentsToShareWith)
1169        .labelled("select residents to share with")
1170}
1171
1172/// parse a system message about items being successfully shared
1173///
1174/// # Errors
1175///
1176/// returns an error if the string could not be parsed
1177#[must_use]
1178pub fn items_successfully_shared_message_parser<'src>()
1179-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1180{
1181    just("Items successfully shared.")
1182        .to(SystemMessage::ItemsSuccessfullyShared)
1183        .labelled("items successfully shared")
1184}
1185
1186/// parse a system message about a modified search query
1187///
1188/// # Errors
1189///
1190/// returns an error if the string could not be parsed
1191#[must_use]
1192pub fn modified_search_query_message_parser<'src>()
1193-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1194{
1195    just("Your search query was modified and the words that were too short were removed.")
1196        .ignore_then(whitespace())
1197        .ignore_then(just("Searched for:"))
1198        .ignore_then(whitespace())
1199        .ignore_then(any().repeated().collect::<String>())
1200        .try_map(|query, _span: chumsky::span::SimpleSpan| {
1201            Ok(SystemMessage::ModifiedSearchQuery { query })
1202        })
1203        .labelled("your search query was modified")
1204}
1205
1206/// parse a system message about a different simulator version
1207///
1208/// # Errors
1209///
1210/// returns an error if the string could not be parsed
1211#[must_use]
1212pub fn simulator_version_message_parser<'src>()
1213-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1214{
1215    just("The region you have entered is running a different simulator version.")
1216        .ignore_then(whitespace())
1217        .ignore_then(just("Current simulator:"))
1218        .ignore_then(whitespace())
1219        .ignore_then(take_until!(just("\n")))
1220        .then_ignore(whitespace())
1221        .then_ignore(just("Previous simulator:"))
1222        .then_ignore(whitespace())
1223        .then(any().repeated().collect::<String>())
1224        .try_map(
1225            |((current_region_simulator_version, _), previous_region_simulator_version),
1226             _span: chumsky::span::SimpleSpan| {
1227                Ok(SystemMessage::SimulatorVersion {
1228                    previous_region_simulator_version,
1229                    current_region_simulator_version,
1230                })
1231            },
1232        )
1233        .labelled("region running different simulator version")
1234}
1235
1236/// parse a system message about a renamed avatar
1237///
1238/// # Errors
1239///
1240/// returns an error if the string could not be parsed
1241#[must_use]
1242pub fn renamed_avatar_message_parser<'src>()
1243-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1244{
1245    take_until!(just(" is now known as"))
1246        .then_ignore(whitespace())
1247        .then(take_until!(just(".").ignore_then(end())))
1248        .try_map(
1249            |((old_name, _), (new_name, ())), _span: chumsky::span::SimpleSpan| {
1250                Ok(SystemMessage::RenamedAvatar { old_name, new_name })
1251            },
1252        )
1253        .labelled("avatar is now known as")
1254}
1255
1256/// parse a system message about enabling or disabling of double-click teleports
1257///
1258/// # Errors
1259///
1260/// returns an error if the string could not be parsed
1261#[must_use]
1262pub fn doubleclick_teleport_message_parser<'src>()
1263-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1264{
1265    just("DoubleClick Teleport enabled.")
1266        .to(SystemMessage::DoubleClickTeleport { enabled: true })
1267        .or(just("DoubleClick Teleport disabled.")
1268            .to(SystemMessage::DoubleClickTeleport { enabled: false }))
1269        .labelled("double click teleport enabled/disabled")
1270}
1271
1272/// parse a system message about enabling or disabling of always run
1273///
1274/// # Errors
1275///
1276/// returns an error if the string could not be parsed
1277#[must_use]
1278pub fn always_run_message_parser<'src>()
1279-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1280{
1281    just("Always Run enabled.")
1282        .to(SystemMessage::AlwaysRun { enabled: true })
1283        .or(just("Always Run disabled.").to(SystemMessage::AlwaysRun { enabled: false }))
1284        .labelled("always run enabled/disabled")
1285}
1286
1287/// parse a system message about being added as an estate manager
1288///
1289/// # Errors
1290///
1291/// returns an error if the string could not be parsed
1292#[must_use]
1293pub fn added_as_estate_manager_message_parser<'src>()
1294-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1295{
1296    just("You have been added as an estate manager.")
1297        .to(SystemMessage::AddedAsEstateManager)
1298        .labelled("you have been added as estate manager")
1299}
1300
1301/// parse a system message about the LSL viewer bridge
1302///
1303/// # Errors
1304///
1305/// returns an error if the string could not be parsed
1306#[must_use]
1307pub fn bridge_message_parser<'src>()
1308-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1309{
1310    choice([
1311    just("Creating the bridge. This might take a moment, please wait.").to(SystemMessage::CreatingBridge).boxed(),
1312    just("Creating the bridge. This might take a few moments, please wait").to(SystemMessage::CreatingBridge).boxed(),
1313    just("Bridge created.").to(SystemMessage::BridgeCreated).boxed(),
1314    just("Bridge creation in process, cannot start another. Please wait a few minutes before trying again.").to(SystemMessage::BridgeCreationInProgress).boxed(),
1315    just("Bridge object not found. Can't proceed with creation, exiting.").to(SystemMessage::BridgeObjectNotFoundCantProceedWithCreation).boxed(),
1316    just("Bridge failed to attach. This is not the current bridge version. Please use the Firestorm 'Avatar/Avatar Health/Recreate Bridge' menu option to recreate the bridge.").to(SystemMessage::BridgeFailedToAttach).boxed(),
1317    just("Bridge failed to attach. Something else was using the bridge attachment point. Please try to recreate the bridge.").to(SystemMessage::BridgeFailedToAttachDueToBridgeAttachmentPointInUse).boxed(),
1318    just("Bridge failed to attach. Something else was using the bridge attachment point. Please use the Firestorm 'Avatar/Avatar Health/Recreate Bridge' menu option to recreate the bridge.").to(SystemMessage::BridgeFailedToAttachDueToBridgeAttachmentPointInUse).boxed(),
1319    just("Bridge not created. The bridge couldn't be found in inventory. Please use the Firestorm 'Avatar/Avatar Health/Recreate Bridge' menu option to recreate the bridge.").to(SystemMessage::BridgeNotCreated).boxed(),
1320    just("Bridge detached.").to(SystemMessage::BridgeDetached).labelled("bridge message").boxed(),
1321])
1322}
1323
1324/// parse a system message about a failure to place an object a a specified
1325/// location
1326///
1327/// # Errors
1328///
1329/// returns an error if the string could not be parsed
1330#[must_use]
1331pub fn failed_to_place_object_at_specified_location_message_parser<'src>()
1332-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1333{
1334    just("Failed to place object at specified location.  Please try again.")
1335        .to(SystemMessage::FailedToPlaceObjectAtSpecifiedLocation)
1336        .labelled("failed to place object at specified location")
1337}
1338
1339/// parse a system message about a changed script count in the current region
1340///
1341/// # Errors
1342///
1343/// returns an error if the string could not be parsed
1344#[must_use]
1345pub fn region_script_count_change_message_parser<'src>()
1346-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1347{
1348    just("Total scripts in region ")
1349    .ignore_then(just("jumped from ").or(just("dropped from ")))
1350    .ignore_then(
1351        digits(10).collect::<String>()
1352            .then_ignore(just(" to "))
1353            .then(digits(10).collect::<String>())
1354            .then_ignore(just(" ("))
1355            .then(one_of("+-"))
1356            .then(digits(10).collect::<String>())
1357            .then_ignore(just(")."))
1358            .try_map(
1359                |(((previous_script_count, current_script_count), sign), diff): (
1360                    ((String, String), char),
1361                    String,
1362                ),
1363                 span: chumsky::span::SimpleSpan| {
1364                    let previous_span = span;
1365                    let previous_script_count =
1366                        previous_script_count.parse().map_err(|err| {
1367                            chumsky::error::Rich::custom(previous_span, format!(
1368                                "Could not parse previous script count ({previous_script_count}) as u32: {err:?}"
1369                            ))
1370                        })?;
1371                    let current_span = span;
1372                    let current_script_count = current_script_count.parse().map_err(|err| {
1373                        chumsky::error::Rich::custom(current_span, format!(
1374                            "Could not parse current script count ({current_script_count}) as u32: {err:?}"
1375                        ))
1376                    })?;
1377                    let diff_span = span;
1378                    let diff: i32 = diff.parse().map_err(|err| {
1379                        chumsky::error::Rich::custom(diff_span, format!(
1380                            "Could not parse changed script count ({diff}) as i32: {err:?}"
1381                        ))
1382                    })?;
1383                    let change = match sign {
1384                        '+' => diff,
1385                        '-' => {
1386                            #[expect(clippy::arithmetic_side_effects, reason = "this just switches the sign of a positive value to a negative one but only the opposite switch is panic-prone at i32::MIN")]
1387                            -diff
1388                        }
1389                        c => {
1390                            return Err(chumsky::error::Rich::custom(span, format!("Unexpected sign character for script change: {c}")))
1391                        }
1392                    };
1393                    Ok(SystemMessage::ScriptCountChanged {
1394                        previous_script_count,
1395                        current_script_count,
1396                        change,
1397                    })
1398                },
1399            ),
1400    )
1401    .labelled("region script count changed")
1402}
1403
1404/// parse a system message about a chat message still being processed
1405///
1406/// # Errors
1407///
1408/// returns an error if the string could not be parsed
1409#[must_use]
1410pub fn chat_message_still_being_processed_message_parser<'src>()
1411-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1412{
1413    just("The message sent to ")
1414    .ignore_then(
1415        choice([
1416            just("Multi-person chat is still being processed.").to(SystemMessage::MultiPersonChatMessageStillBeingProcessed).boxed(),
1417            just("(IM Session Doesn't Exist) is still being processed.").to(SystemMessage::ChatMessageToNoLongerExistingImSessionStillBeingProcessed).boxed(),
1418            just("Conference with ").ignore_then(take_until!(just(" is still being processed.")).map(|(avatar_name, _)| SystemMessage::ConferenceChatMessageStillBeingProcessed { avatar_name })).boxed(),
1419            take_until!(just(" is still being processed.").ignored())
1420            .map(|(group_name, ())| {
1421                SystemMessage::GroupChatMessageStillBeingProcessed {
1422                    group_name,
1423                }
1424            }).boxed(),
1425        ])
1426    )
1427    .then_ignore(newline())
1428    .then_ignore(whitespace())
1429    .then_ignore(just("If the message does not appear in the next few minutes, it may have been dropped by the server."))
1430    .labelled("chat message still being processed")
1431}
1432
1433/// parse a system message about an avatar declining a voice call
1434///
1435/// # Errors
1436///
1437/// returns an error if the string could not be parsed
1438#[must_use]
1439pub fn avatar_declined_voice_call_message_parser<'src>()
1440-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1441{
1442    take_until!(just(
1443        "has declined your call.  You will now be reconnected to Nearby Voice Chat.",
1444    ))
1445    .map(|(avatar_name, _)| SystemMessage::AvatarDeclinedVoice { avatar_name })
1446    .labelled("avatar declined voice call")
1447}
1448
1449/// parse a system message about an avatar being unavailable to take our voice call
1450///
1451/// # Errors
1452///
1453/// returns an error if the string could not be parsed
1454#[must_use]
1455pub fn avatar_unavailable_for_voice_call_message_parser<'src>()
1456-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1457{
1458    take_until!(just(
1459        "is not available to take your call.  You will now be reconnected to Nearby Voice Chat.",
1460    ))
1461    .map(|(avatar_name, _)| SystemMessage::AvatarUnavailableForVoice { avatar_name })
1462    .labelled("avatar unavailable for voice call")
1463}
1464
1465/// parse a system message about audio from a domain always being played
1466///
1467/// # Errors
1468///
1469/// returns an error if the string could not be parsed
1470#[must_use]
1471pub fn audio_from_domain_will_always_be_played_message_parser<'src>()
1472-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1473{
1474    just("Audio from the domain ")
1475        .ignore_then(
1476            take_until!(just(" will always be played."))
1477                .map(|(domain, _)| SystemMessage::AudioFromDomainWillAlwaysBePlayed { domain }),
1478        )
1479        .labelled("audio from domain will always be played")
1480}
1481
1482/// parse a system message about an object not being for sale
1483///
1484/// # Errors
1485///
1486/// returns an error if the string could not be parsed
1487#[must_use]
1488pub fn object_not_for_sale_message_parser<'src>()
1489-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1490{
1491    just("This object is not for sale.")
1492        .to(SystemMessage::ObjectNotForSale)
1493        .labelled("object not for sale")
1494}
1495
1496/// parse a system message cannot create requested inventory
1497///
1498/// # Errors
1499///
1500/// returns an error if the string could not be parsed
1501#[must_use]
1502pub fn can_not_create_requested_inventory_message_parser<'src>()
1503-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1504{
1505    just("Cannot create requested inventory.")
1506        .to(SystemMessage::CanNotCreateRequestedInventory)
1507        .labelled("cannot created requested inventory")
1508}
1509
1510/// parse a system message about a failed link due to piece distance
1511///
1512/// # Errors
1513///
1514/// returns an error if the string could not be parsed
1515#[must_use]
1516pub fn link_failed_due_to_piece_distance_message_parser<'src>()
1517-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1518{
1519    just("Link failed -- Unable to link any pieces - pieces are too far apart.")
1520        .to(SystemMessage::LinkFailedDueToPieceDistance {
1521            link_failed_pieces: None,
1522            total_selected_pieces: None,
1523        })
1524        .or(just("Link failed -- Unable to link ").ignore_then(
1525            usize_parser()
1526                .then_ignore(just(" of the "))
1527                .then(usize_parser())
1528                .then_ignore(just(" selected pieces - pieces are too far apart."))
1529                .map(|(link_failed_pieces, total_selected_pieces)| {
1530                    SystemMessage::LinkFailedDueToPieceDistance {
1531                        link_failed_pieces: Some(link_failed_pieces),
1532                        total_selected_pieces: Some(total_selected_pieces),
1533                    }
1534                }),
1535        ))
1536        .labelled("link failed due to distance")
1537}
1538
1539/// parse a system message about the failure to rez an object due to a full
1540/// parcel
1541///
1542/// # Errors
1543///
1544/// returns an error if the string could not be parsed
1545#[must_use]
1546pub fn rezzing_object_failed_due_to_full_parcel_message_parser<'src>()
1547-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1548{
1549    just("Can't rez object '").ignore_then(
1550    take_until!(just("' at ").ignored())
1551        .then(sl_types::map::region_coordinates_parser())
1552        .then_ignore(just(" on parcel '"))
1553        .then(
1554            take_until!(just("' in region ").ignored())
1555        )
1556        .then(
1557            take_until!(just(" because the parcel is too full.").ignored())
1558                .try_map(|(region_name, ()), span| {
1559                    sl_types::map::RegionName::try_new(&region_name).map_err(|err| {
1560                        chumsky::error::Rich::custom(span, format!(
1561                            "Could not turn parsed region name ({region_name}) into RegionName: {err:?}"
1562                        ))
1563                    })
1564                }),
1565        )
1566        .map(
1567            |((((object_name, ()), attempted_rez_location), (parcel_name, ())), region_name)| {
1568                SystemMessage::RezObjectFailedDueToFullParcel {
1569                    object_name,
1570                    attempted_rez_location,
1571                    parcel_name,
1572                    region_name,
1573                }
1574            },
1575        ),
1576    ).labelled("rezzing failed due to full parcel")
1577}
1578
1579/// parse a system message about the failure to create an object due to a full
1580/// region
1581///
1582/// # Errors
1583///
1584/// returns an error if the string could not be parsed
1585#[must_use]
1586pub fn create_object_failed_due_to_full_region_message_parser<'src>()
1587-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1588{
1589    just("Unable to create requested object. The region is full.")
1590        .to(SystemMessage::CreateObjectFailedDueToFullRegion)
1591        .labelled("create object failed due to full region")
1592}
1593
1594/// parse a system message about an object being returned to your inventory
1595///
1596/// # Errors
1597///
1598/// returns an error if the string could not be parsed
1599#[must_use]
1600pub fn your_object_has_been_returned_message_parser<'src>()
1601-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1602{
1603    just("Your object '")
1604        .ignore_then(take_until!(just(
1605            "' has been returned to your inventory Lost and Found folder from parcel '",
1606        )))
1607        .then(take_until!(just("' at ")))
1608        .then(sl_types::map::region_name_parser())
1609        .then_ignore(whitespace())
1610        .then(sl_types::utils::i16_parser())
1611        .then_ignore(just(", "))
1612        .then(sl_types::utils::i16_parser())
1613        .then(
1614            just(" due to parcel auto return.")
1615                .to(true)
1616                .or(just('.').to(false)),
1617        )
1618        .map(
1619            |((((((object_name, _), (parcel_name, _)), region_name), x), y), auto_return)| {
1620                SystemMessage::YourObjectHasBeenReturned {
1621                    object_name,
1622                    parcel_name,
1623                    location: sl_types::map::UnconstrainedLocation::new(region_name, x, y, 0),
1624                    auto_return,
1625                }
1626            },
1627        )
1628        .labelled("your object has been returned")
1629}
1630
1631/// parse a system message about the denial of permission to create an object
1632///
1633/// # Errors
1634///
1635/// returns an error if the string could not be parsed
1636#[must_use]
1637pub fn permission_to_create_object_denied_message_parser<'src>()
1638-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1639{
1640    just("You cannot create objects here.  The owner of this land does not allow it.  Use the land tool to see land ownership.").to(SystemMessage::PermissionToCreateObjectDenied).labelled("permission to create object denied")
1641}
1642
1643/// parse a system message about the denial of permission to rez an object
1644///
1645/// # Errors
1646///
1647/// returns an error if the string could not be parsed
1648#[must_use]
1649pub fn permission_to_rez_object_denied_message_parser<'src>()
1650-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1651{
1652    just("Can't rez object '")
1653    .ignore_then(
1654        take_until!(just("' at ").ignored())
1655        .then(sl_types::map::region_coordinates_parser())
1656        .then_ignore(just(" on parcel '"))
1657        .then(take_until!(just("' in region ").ignored()))
1658        .then(take_until!(just(" because the owner of this land does not allow it.  Use the land tool to see land ownership.").ignored()).try_map(|(region_name, ()), span| {
1659            sl_types::map::RegionName::try_new(&region_name).map_err(|err| chumsky::error::Rich::custom(span, format!("Could not turn parsed region name ({region_name}) into RegionName: {err:?}")))
1660        }))
1661        .map(|((((object_name, ()), attempted_rez_location), (parcel_name, ())), region_name)| {
1662            SystemMessage::PermissionToRezObjectDenied {
1663                object_name,
1664                attempted_rez_location,
1665                parcel_name,
1666                region_name,
1667            }
1668        })
1669    )
1670    .labelled("permission to rez object denied")
1671}
1672
1673/// parse a system message about the denial of permission to reposition an object
1674///
1675/// # Errors
1676///
1677/// returns an error if the string could not be parsed
1678#[must_use]
1679pub fn permission_to_reposition_denied_message_parser<'src>()
1680-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1681{
1682    just("Can't reposition -- permission denied")
1683        .to(SystemMessage::PermissionToRepositionDenied)
1684        .labelled("permission to reposition denied")
1685}
1686
1687/// parse a system message about the denial of permission to rotate an object
1688///
1689/// # Errors
1690///
1691/// returns an error if the string could not be parsed
1692#[must_use]
1693pub fn permission_to_rotate_denied_message_parser<'src>()
1694-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1695{
1696    just("Can't rotate -- permission denied")
1697        .to(SystemMessage::PermissionToRotateDenied)
1698        .labelled("permission to rotate denied")
1699}
1700
1701/// parse a system message about the denial of permission to rescale an object
1702///
1703/// # Errors
1704///
1705/// returns an error if the string could not be parsed
1706#[must_use]
1707pub fn permission_to_rescale_denied_message_parser<'src>()
1708-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1709{
1710    just("Can't rescale -- permission denied")
1711        .to(SystemMessage::PermissionToRescaleDenied)
1712        .labelled("permission to rescale denied")
1713}
1714
1715/// parse a system message about the denial of permission to unlink an object
1716/// because build permissions are missing on at least one parcel
1717///
1718/// # Errors
1719///
1720/// returns an error if the string could not be parsed
1721#[must_use]
1722pub fn permission_to_unlink_denied_due_to_missing_parcel_build_permissions_message_parser<'src>()
1723-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1724{
1725    just("Failed to unlink because you do not have permissions to build on all parcels")
1726        .to(SystemMessage::PermissionToUnlinkDeniedDueToMissingParcelBuildPermissions)
1727        .labelled("permission to unlink denied due to parcel build permissions")
1728}
1729
1730/// parse a system message about the denial of permission to view a script
1731///
1732/// # Errors
1733///
1734/// returns an error if the string could not be parsed
1735#[must_use]
1736pub fn permission_to_view_script_denied_message_parser<'src>()
1737-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1738{
1739    just("Insufficient permissions to view the script.")
1740        .to(SystemMessage::PermissionToViewScriptDenied)
1741        .labelled("permission to view script denied")
1742}
1743
1744/// parse a system message about the denial of permission to view a notecard
1745///
1746/// # Errors
1747///
1748/// returns an error if the string could not be parsed
1749#[must_use]
1750pub fn permission_to_view_notecard_denied_message_parser<'src>()
1751-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1752{
1753    just("You do not have permission to view this notecard.")
1754        .to(SystemMessage::PermissionToViewNotecardDenied)
1755        .labelled("permission to view notecard denied")
1756}
1757
1758/// parse a system message about the denial of permission to change a shape
1759///
1760/// # Errors
1761///
1762/// returns an error if the string could not be parsed
1763#[must_use]
1764pub fn permission_to_change_shape_denied_message_parser<'src>()
1765-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1766{
1767    just("You are not allowed to change this shape.")
1768        .to(SystemMessage::PermissionToChangeShapeDenied)
1769        .labelled("permission to change shape denied")
1770}
1771
1772/// parse a system message about the denial of permission to enter a parcel
1773///
1774/// # Errors
1775///
1776/// returns an error if the string could not be parsed
1777#[must_use]
1778pub fn permission_to_enter_parcel_denied_message_parser<'src>()
1779-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1780{
1781    just("Cannot enter parcel, you are not on the access list.")
1782        .to(SystemMessage::PermissionToEnterParcelDenied)
1783        .labelled("permission to enter parcel denied")
1784}
1785
1786/// parse a system message about the denial of permission to enter a parcel due to ban
1787///
1788/// # Errors
1789///
1790/// returns an error if the string could not be parsed
1791#[must_use]
1792pub fn permission_to_enter_parcel_denied_due_to_ban_message_parser<'src>()
1793-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1794{
1795    just("Cannot enter parcel, you have been banned.")
1796        .to(SystemMessage::PermissionToEnterParcelDeniedDueToBan)
1797        .labelled("permission to enter parcel denied due to ban")
1798}
1799
1800/// parse a system message about ejecting an avatar from a parcel
1801///
1802/// # Errors
1803///
1804/// returns an error if the string could not be parsed
1805#[must_use]
1806pub fn avatar_ejected_message_parser<'src>()
1807-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1808{
1809    just("Avatar ejected.")
1810        .to(SystemMessage::EjectedAvatar)
1811        .labelled("avatar ejected")
1812}
1813
1814/// parse a system message about being ejected from a parcel
1815///
1816/// # Errors
1817///
1818/// returns an error if the string could not be parsed
1819#[must_use]
1820pub fn ejected_from_parcel_message_parser<'src>()
1821-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1822{
1823    just("You have been ejected from this land.")
1824        .to(SystemMessage::EjectedFromParcel)
1825        .or(
1826            just("You are no longer allowed here and have been ejected.")
1827                .to(SystemMessage::EjectedFromParcelBecauseNoLongerAllowed),
1828        )
1829        .labelled("ejected from parcel")
1830}
1831
1832/// parse a system message about being banned from a parcel
1833///
1834/// # Errors
1835///
1836/// returns an error if the string could not be parsed
1837#[must_use]
1838pub fn banned_from_parcel_message_parser<'src>()
1839-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1840{
1841    just("You have been banned ")
1842        .ignore_then(
1843            just("indefinitely")
1844                .to(SystemMessage::BannedFromParcelIndefinitely)
1845                .or(just("for ")
1846                    .ignore_then(i64_parser().then_ignore(just(" minutes")))
1847                    .map(|d| SystemMessage::BannedFromParcelTemporarily {
1848                        ban_duration: time::Duration::minutes(d),
1849                    })),
1850        )
1851        .labelled("you have been banned")
1852}
1853
1854/// parse a system message about only group members being able to visit an area
1855///
1856/// # Errors
1857///
1858/// returns an error if the string could not be parsed
1859#[must_use]
1860pub fn only_group_members_can_visit_this_area_message_parser<'src>()
1861-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1862{
1863    just("Only members of a certain group can visit this area.")
1864        .to(SystemMessage::OnlyGroupMembersCanVisitThisArea)
1865        .labelled("only group members allowed here")
1866}
1867
1868/// parse a system message about teleports being RLV restricted
1869///
1870/// # Errors
1871///
1872/// returns an error if the string could not be parsed
1873#[must_use]
1874pub fn unable_to_teleport_due_to_rlv_message_parser<'src>()
1875-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1876{
1877    just("Unable to initiate teleport due to RLV restrictions")
1878        .to(SystemMessage::UnableToTeleportDueToRlv)
1879        .labelled("unable to teleport due to RLV")
1880}
1881
1882/// parse a system message about opening textures being RLV restricted
1883///
1884/// # Errors
1885///
1886/// returns an error if the string could not be parsed
1887#[must_use]
1888pub fn unable_to_open_texture_due_to_rlv_message_parser<'src>()
1889-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1890{
1891    just("Unable to open texture due to RLV restrictions")
1892        .to(SystemMessage::UnableToOpenTextureDueToRlv)
1893        .labelled("unable to open texture due to RLV")
1894}
1895
1896/// parse a system message about unsupported SLurl
1897///
1898/// # Errors
1899///
1900/// returns an error if the string could not be parsed
1901#[must_use]
1902pub fn unsupported_slurl_message_parser<'src>()
1903-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1904{
1905    just("The SLurl you clicked on is not supported.")
1906        .to(SystemMessage::UnsupportedSlurl)
1907        .labelled("unsupported slurl")
1908}
1909
1910/// parse a system message about a SLurl from an untrusted browser being blocked
1911///
1912/// # Errors
1913///
1914/// returns an error if the string could not be parsed
1915#[must_use]
1916pub fn blocked_untrusted_browser_slurl_message_parser<'src>()
1917-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1918{
1919    just("A SLurl was received from an untrusted browser and has been blocked for your security.")
1920        .to(SystemMessage::BlockedUntrustedBrowserSlurl)
1921        .labelled("blocked untrusted browser")
1922}
1923
1924/// parse a system message about a grid status error about an invalid message format
1925///
1926/// # Errors
1927///
1928/// returns an error if the string could not be parsed
1929#[must_use]
1930pub fn grid_status_error_invalid_message_format_message_parser<'src>()
1931-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1932{
1933    just("SL Grid Status error: Invalid message format. Try again later.")
1934        .to(SystemMessage::GridStatusErrorInvalidMessageFormat)
1935        .labelled("sl grid status invalid message format")
1936}
1937
1938/// parse a system message about a script info object being invalid or out of range
1939///
1940/// # Errors
1941///
1942/// returns an error if the string could not be parsed
1943#[must_use]
1944pub fn script_info_object_invalid_or_out_of_range_message_parser<'src>()
1945-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1946{
1947    just("Script info: Object to check is invalid or out of range.")
1948        .to(SystemMessage::ScriptInfoObjectInvalidOrOutOfRange)
1949        .labelled("script info object invalid or out of range")
1950}
1951
1952/// parse a system message about script info
1953///
1954/// # Errors
1955///
1956/// returns an error if the string could not be parsed
1957#[must_use]
1958pub fn script_info_message_parser<'src>()
1959-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
1960{
1961    just("Script info: '")
1962        .ignore_then(
1963            take_until!(just("': [").ignored())
1964                .then(usize_parser())
1965                .then_ignore(just('/'))
1966                .then(usize_parser())
1967                .then_ignore(just("] running scripts, "))
1968                .then(u64_parser().map(bytesize::ByteSize::kb))
1969                .then_ignore(just(" KB allowed memory size limit, "))
1970                .then(unsigned_f32_parser().map(|ms| time::Duration::seconds_f32(ms / 1000f32)))
1971                .then_ignore(just(" ms of CPU time consumed."))
1972                .map(
1973                    |(
1974                        ((((name, ()), running_scripts), total_scripts), allowed_memory_size_limit),
1975                        cpu_time_consumed,
1976                    )| {
1977                        SystemMessage::ScriptInfo {
1978                            name,
1979                            running_scripts,
1980                            total_scripts,
1981                            allowed_memory_size_limit,
1982                            cpu_time_consumed,
1983                        }
1984                    },
1985                ),
1986        )
1987        .labelled("script info")
1988}
1989
1990/// parse a system message with extended script info
1991/// usually this should follow a line with regular script info containing the
1992/// object name
1993///
1994/// # Errors
1995///
1996/// returns an error if the string could not be parsed
1997#[must_use]
1998pub fn extended_script_info_message_parser<'src>()
1999-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2000{
2001    just("Object ID: ")
2002    .ignore_then(sl_types::key::object_key_parser())
2003    .then_ignore(newline())
2004    .then_ignore(just(" Description:"))
2005    .then_ignore(just(" ").or_not())
2006    .then(just("(No Description)").then_ignore(newline()).to(None).or(
2007        take_until!(newline().ignored()).map(|(vc, ())| Some(vc)),
2008    ))
2009    .then_ignore(just(" Root prim: "))
2010    .then(sl_types::key::object_key_parser())
2011    .then_ignore(newline())
2012    .then_ignore(just(" Prim count: "))
2013    .then(sl_types::utils::usize_parser())
2014    .then_ignore(newline())
2015    .then_ignore(just(" Land impact: "))
2016    .then(sl_types::utils::usize_parser())
2017    .then_ignore(newline())
2018    .then_ignore(just(" Inventory items: "))
2019    .then(sl_types::utils::usize_parser())
2020    .then_ignore(newline())
2021    .then_ignore(just(" Velocity: "))
2022    .then(sl_types::lsl::vector_parser())
2023    .then_ignore(newline())
2024    .then_ignore(just(" Position: "))
2025    .then(sl_types::lsl::vector_parser().map(sl_types::map::RegionCoordinates::from))
2026    .then_ignore(whitespace())
2027    .then(sl_types::map::distance_parser().delimited_by(just('('), just(')')))
2028    .then_ignore(newline())
2029    .then_ignore(just(" Rotation: "))
2030    .then(sl_types::lsl::rotation_parser())
2031    .then_ignore(whitespace())
2032    .then(sl_types::lsl::vector_parser().delimited_by(just('('), just(')')))
2033    .then_ignore(newline())
2034    .then_ignore(just(" Angular velocity: "))
2035    .then(sl_types::lsl::vector_parser())
2036    .then_ignore(whitespace())
2037    .then_ignore(just("(radians per second)"))
2038    .then_ignore(newline())
2039    .then_ignore(just(" Creator: "))
2040    .then(sl_types::key::app_agent_uri_as_agent_key_parser())
2041    .then_ignore(newline())
2042    .then_ignore(just(" Owner: "))
2043    .then(just("Group Owned").to(None).or(sl_types::key::app_agent_or_group_uri_as_owner_key_parser().map(Some)))
2044    .then_ignore(newline())
2045    .then_ignore(just(" Previous owner: "))
2046    .then(
2047        sl_types::key::app_agent_or_group_uri_as_owner_key_parser()
2048            .map(Some)
2049            .or(just("---").to(None)),
2050    )
2051    .then_ignore(newline())
2052    .then_ignore(just(" Rezzed by: "))
2053    .then(sl_types::key::agent_key_parser())
2054    .then_ignore(newline())
2055    .then_ignore(just(" Group: "))
2056    .then(
2057        sl_types::key::app_group_uri_as_group_key_parser()
2058            .map(Some)
2059            .or(just("---").to(None)),
2060    )
2061    .then_ignore(newline())
2062    .then_ignore(just(" Creation time:"))
2063    .then_ignore(just(' ').or_not())
2064    .then(crate::utils::offset_datetime_parser().or_not())
2065    .then_ignore(newline())
2066    .then_ignore(just(" Rez time:"))
2067    .then_ignore(just(' ').or_not())
2068    .then(crate::utils::offset_datetime_parser().or_not())
2069    .then_ignore(newline())
2070    .then_ignore(just(" Pathfinding type: "))
2071    .then(sl_types::pathfinding::int_as_pathfinding_type_parser())
2072    .then_ignore(newline())
2073    .then_ignore(just(" Attachment point: "))
2074    .then(
2075        sl_types::attachment::attachment_point_parser()
2076            .map(Some)
2077            .or(just("---").to(None)),
2078    )
2079    .then_ignore(newline())
2080    .then_ignore(just(" Temporarily attached: "))
2081    .then(just("Yes").to(true).or(just("No").to(false)))
2082    .then_ignore(newline())
2083    .then_ignore(just(" Your current position: "))
2084    .then(sl_types::lsl::vector_parser().map(sl_types::map::RegionCoordinates::from))
2085    .map(
2086        |((((((((((((((((((((((
2087            object_key,
2088            description),
2089            root_prim),
2090            prim_count),
2091            land_impact),
2092            inventory_items),
2093            velocity),
2094            position),
2095            position_distance),
2096            rotation),
2097            rotation_vector_degrees),
2098            angular_velocity),
2099            creator),
2100            owner),
2101            previous_owner),
2102            rezzed_by),
2103            group),
2104            creation_time),
2105            rez_time),
2106            pathfinding_type),
2107            attachment_point),
2108            temporarily_attached),
2109            inspecting_avatar_position,
2110        )| {
2111            SystemMessage::ExtendedScriptInfo {
2112                object_key,
2113                description,
2114                root_prim,
2115                prim_count,
2116                land_impact,
2117                inventory_items,
2118                velocity,
2119                position,
2120                position_distance,
2121                rotation,
2122                rotation_vector_degrees,
2123                angular_velocity,
2124                creator,
2125                owner,
2126                previous_owner,
2127                rezzed_by,
2128                group,
2129                creation_time,
2130                rez_time,
2131                pathfinding_type,
2132                attachment_point,
2133                temporarily_attached,
2134                inspecting_avatar_position,
2135            }
2136        },
2137    ).labelled("extended script info")
2138}
2139
2140/// parse a system message about dice rolls
2141///
2142/// # Errors
2143///
2144/// returns an error if the string could not be parsed
2145#[must_use]
2146pub fn dice_roll_message_parser<'src>()
2147-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2148{
2149    choice([
2150        just("You must provide positive values for dice (max 100) and faces (max 1000).")
2151            .to(SystemMessage::DiceRollCommandUsageInstructions)
2152            .boxed(),
2153        just('#')
2154            .ignore_then(usize_parser())
2155            .then_ignore(whitespace())
2156            .then_ignore(just("1d"))
2157            .then(usize_parser())
2158            .then_ignore(just(":"))
2159            .then_ignore(whitespace())
2160            .then(usize_parser())
2161            .then_ignore(just('.'))
2162            .map(
2163                |((roll_number, dice_faces), roll_result)| SystemMessage::DiceRollResult {
2164                    roll_number,
2165                    dice_faces,
2166                    roll_result,
2167                },
2168            )
2169            .boxed(),
2170        just("Total result for ")
2171            .ignore_then(usize_parser())
2172            .then_ignore(just('d'))
2173            .then(usize_parser())
2174            .then_ignore(just(':'))
2175            .then_ignore(whitespace())
2176            .then(usize_parser())
2177            .then_ignore(just('.'))
2178            .map(
2179                |((roll_count, dice_faces), result_sum)| SystemMessage::DiceRollResultSum {
2180                    roll_count,
2181                    dice_faces,
2182                    result_sum,
2183                },
2184            )
2185            .boxed(),
2186    ])
2187    .labelled("dice roll")
2188}
2189
2190/// parse a system message about object textures on faces
2191///
2192/// # Errors
2193///
2194/// returns an error if the string could not be parsed
2195#[must_use]
2196pub fn texture_info_message_parser<'src>()
2197-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2198{
2199    choice([
2200        just("Texture info for: ")
2201            .ignore_then(
2202                take_until!(newline().or(end()))
2203                    .map(|(object_name, ())| SystemMessage::TextureInfoForObject { object_name }),
2204            )
2205            .boxed(),
2206        sl_types::utils::u16_parser()
2207            .then_ignore(just('x'))
2208            .then(sl_types::utils::u16_parser())
2209            .then_ignore(whitespace())
2210            .then(just("opaque").or(just("alpha")))
2211            .then_ignore(just(" on face "))
2212            .then(usize_parser())
2213            .map(
2214                |(((texture_width, texture_height), texture_type), face_number)| {
2215                    SystemMessage::TextureInfoForFace {
2216                        face_number,
2217                        texture_width,
2218                        texture_height,
2219                        texture_type: texture_type.to_owned(),
2220                    }
2221                },
2222            )
2223            .boxed(),
2224    ])
2225    .labelled("texture info")
2226}
2227
2228/// parse a system message by the Firestorm developers
2229///
2230/// # Errors
2231///
2232/// returns an error if the string could not be parsed
2233#[must_use]
2234pub fn firestorm_message_parser<'src>()
2235-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2236{
2237    just("Firestorm ")
2238        .ignore_then(
2239            take_until!(just("!").ignored())
2240                .map(|(message_type, ())| message_type)
2241                .then(any().repeated().collect::<String>())
2242                .map(|(message_type, message)| SystemMessage::FirestormMessage {
2243                    message_type,
2244                    message,
2245                }),
2246        )
2247        .labelled("firestorm message")
2248}
2249
2250/// parse a system message about a grid status event
2251///
2252/// # Errors
2253///
2254/// returns an error if the string could not be parsed
2255#[must_use]
2256pub fn grid_status_event_message_parser<'src>()
2257-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2258{
2259    just("[ ")
2260        .ignore_then(
2261            take_until!(just(" ] "))
2262                .map(|(vc, _)| vc)
2263                .then(
2264                    just("THIS IS A SCHEDULED EVENT ")
2265                        .or_not()
2266                        .map(|s| s.is_some()),
2267                )
2268                .then(
2269                    take_until!(just(" [ https://status.secondlifegrid.net/incidents/").ignored())
2270                        .map(|(vc, ())| vc),
2271                )
2272                .then(take_until!(just(' ').ignored()).map(|(vc, ())| vc))
2273                .then_ignore(just("]"))
2274                .map(
2275                    |(((title, scheduled), body), url_fragment)| SystemMessage::GridStatusEvent {
2276                        title,
2277                        scheduled,
2278                        body,
2279                        incident_url: format!(
2280                            "https://status.secondlifegird.net/incidents/{url_fragment}"
2281                        ),
2282                    },
2283                ),
2284        )
2285        .labelled("grid status event")
2286}
2287
2288/// parse a Second Life system message
2289///
2290/// # Errors
2291///
2292/// returns an error if the string could not be parsed
2293#[must_use]
2294pub fn system_message_parser<'src>()
2295-> impl Parser<'src, &'src str, SystemMessage, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
2296{
2297    choice([
2298    snapshot_saved_message_parser().boxed(),
2299    attachment_saved_message_parser().boxed(),
2300    draw_distance_set_message_parser().boxed(),
2301    home_position_set_message_parser().boxed(),
2302    land_divided_message_parser().boxed(),
2303    failed_to_join_land_due_to_region_boundary_message_parser().boxed(),
2304    offered_calling_card_message_parser().boxed(),
2305    you_paid_for_object_message_parser().boxed(),
2306    you_paid_to_create_a_group_message_parser().boxed(),
2307    you_paid_to_join_group_message_parser().boxed(),
2308    you_paid_for_land_message_parser().boxed(),
2309    failed_to_pay_message_parser().boxed(),
2310    object_granted_permission_to_take_money_parser().boxed(),
2311    sent_payment_message_parser().boxed(),
2312    received_payment_message_parser().boxed(),
2313    group_membership_message_parser().boxed(),
2314    unable_to_invite_user_due_to_missing_group_membership_message_parser().boxed(),
2315    unable_to_invite_user_due_to_differing_limited_estate_message_parser().boxed(),
2316    unable_to_load_notecard_message_parser().boxed(),
2317    unable_to_load_gesture_message_parser().boxed(),
2318    teleport_completed_message_parser().boxed(),
2319    now_playing_message_parser().boxed(),
2320    region_restart_message_parser().boxed(),
2321    object_gave_object_message_parser().boxed(),
2322    object_gave_folder_message_parser().boxed(),
2323    declined_given_object_message_parser().boxed(),
2324    select_residents_to_share_with_message_parser().boxed(),
2325    items_successfully_shared_message_parser().boxed(),
2326    modified_search_query_message_parser().boxed(),
2327    avatar_gave_object_message_parser().boxed(),
2328    simulator_version_message_parser().boxed(),
2329    renamed_avatar_message_parser().boxed(),
2330    doubleclick_teleport_message_parser().boxed(),
2331    always_run_message_parser().boxed(),
2332    added_as_estate_manager_message_parser().boxed(),
2333    bridge_message_parser().boxed(),
2334    failed_to_place_object_at_specified_location_message_parser().boxed(),
2335    region_script_count_change_message_parser().boxed(),
2336    chat_message_still_being_processed_message_parser().boxed(),
2337    avatar_declined_voice_call_message_parser().boxed(),
2338    avatar_unavailable_for_voice_call_message_parser().boxed(),
2339    audio_from_domain_will_always_be_played_message_parser().boxed(),
2340    object_not_for_sale_message_parser().boxed(),
2341    can_not_create_requested_inventory_message_parser().boxed(),
2342    link_failed_due_to_piece_distance_message_parser().boxed(),
2343    rezzing_object_failed_due_to_full_parcel_message_parser().boxed(),
2344    create_object_failed_due_to_full_region_message_parser().boxed(),
2345    your_object_has_been_returned_message_parser().boxed(),
2346    permission_to_create_object_denied_message_parser().boxed(),
2347    permission_to_rez_object_denied_message_parser().boxed(),
2348    permission_to_reposition_denied_message_parser().boxed(),
2349    permission_to_rotate_denied_message_parser().boxed(),
2350    permission_to_rescale_denied_message_parser().boxed(),
2351    permission_to_unlink_denied_due_to_missing_parcel_build_permissions_message_parser()
2352        .boxed(),
2353    permission_to_view_script_denied_message_parser().boxed(),
2354    permission_to_view_notecard_denied_message_parser().boxed(),
2355    permission_to_change_shape_denied_message_parser().boxed(),
2356    permission_to_enter_parcel_denied_message_parser().boxed(),
2357    permission_to_enter_parcel_denied_due_to_ban_message_parser().boxed(),
2358    avatar_ejected_message_parser().boxed(),
2359    ejected_from_parcel_message_parser().boxed(),
2360    banned_from_parcel_message_parser().boxed(),
2361    only_group_members_can_visit_this_area_message_parser().boxed(),
2362    unable_to_teleport_due_to_rlv_message_parser().boxed(),
2363    unable_to_open_texture_due_to_rlv_message_parser().boxed(),
2364    unsupported_slurl_message_parser().boxed(),
2365    blocked_untrusted_browser_slurl_message_parser().boxed(),
2366    grid_status_error_invalid_message_format_message_parser().boxed(),
2367    script_info_object_invalid_or_out_of_range_message_parser().boxed(),
2368    script_info_message_parser().boxed(),
2369    extended_script_info_message_parser().boxed(),
2370    dice_roll_message_parser().boxed(),
2371    texture_info_message_parser().boxed(),
2372    firestorm_message_parser().boxed(),
2373    grid_status_event_message_parser().boxed(),
2374    take_until!(just("https").or(just("http")).or(just("Http")))
2375        .then(take_until!(newline().or(end())).map(|(vc, ())| vc))
2376        .map(
2377            |((message, scheme), rest_of_url)| SystemMessage::SystemMessageWithLink {
2378                message,
2379                link: format!("{scheme}://{rest_of_url}"),
2380            },
2381        )
2382        .boxed(),
2383    take_until!(just("www."))
2384        .then(take_until!(newline().or(end())).map(|(vc, ())| vc))
2385        .map(
2386            |((message, subdomain), rest_of_url)| SystemMessage::SystemMessageWithLink {
2387                message,
2388                link: format!("{subdomain}{rest_of_url}"),
2389            },
2390        )
2391        .boxed(),
2392    any()
2393        .repeated()
2394        .collect::<String>()
2395        .map(|message| {
2396            if message.contains("Firestorm") && (message.contains("holiday") || message.contains("Happy New Year")) {
2397                SystemMessage::FirestormHolidayWishes { message }
2398            } else if message.contains("phishing") {
2399                SystemMessage::PhishingWarning { message }
2400            } else if message == "This is a test version of Firestorm. If this were an actual release version, a real message of the day would be here. This is only a test." {
2401                SystemMessage::TestMessageOfTheDay
2402            } else if (message.ends_with("...") && (message.starts_with("Loading") || message.starts_with("Initializing") || message.starts_with("Downloading") || message.starts_with("Verifying") || message.starts_with("Loading") || message.starts_with("Connecting") || message.starts_with("Decoding") || message.starts_with("Waiting"))) || message == "Welcome to Advertisement-Free Firestorm" || message.starts_with("Logging in") {
2403                SystemMessage::EarlyFirestormStartupMessage { message }
2404            } else if message.contains("wiki.phoenixviewer.com/firestorm_classes") {
2405                SystemMessage::FirestormMessage {
2406                    message_type: "Classes".to_string(),
2407                    message,
2408                }
2409            } else if message.contains("BETA TESTERS") || message.contains("Beta Testers") {
2410                SystemMessage::FirestormMessage {
2411                    message_type: "Beta Test".to_string(),
2412                    message,
2413                }
2414            } else {
2415                SystemMessage::OtherSystemMessage { message }
2416            }
2417        })
2418        .boxed(),
2419])
2420}
2421
2422#[cfg(test)]
2423mod test {
2424    use super::*;
2425    use pretty_assertions::assert_eq;
2426
2427    #[test]
2428    fn test_teleport_completed() -> Result<(), Box<dyn std::error::Error>> {
2429        assert_eq!(
2430            Ok(SystemMessage::TeleportCompleted {
2431                origin: sl_types::map::UnconstrainedLocation {
2432                    region_name: sl_types::map::RegionName::try_new("Fudo")?,
2433                    x: 30,
2434                    y: 169,
2435                    z: 912
2436                }
2437            }),
2438            teleport_completed_message_parser()
2439                .parse(
2440                    "Teleport completed from http://maps.secondlife.com/secondlife/Fudo/30/169/912"
2441                )
2442                .into_result()
2443        );
2444        Ok(())
2445    }
2446
2447    #[test]
2448    fn test_teleport_completed_extra_short() -> Result<(), Box<dyn std::error::Error>> {
2449        assert_eq!(
2450            Ok(SystemMessage::TeleportCompleted {
2451                origin: sl_types::map::UnconstrainedLocation {
2452                    region_name: sl_types::map::RegionName::try_new("AA")?,
2453                    x: 78,
2454                    y: 83,
2455                    z: 26
2456                }
2457            }),
2458            teleport_completed_message_parser()
2459                .parse("Teleport completed from http://maps.secondlife.com/secondlife/AA/78/83/26")
2460                .into_result()
2461        );
2462        Ok(())
2463    }
2464
2465    #[test]
2466    fn test_cant_rez_object() -> Result<(), Box<dyn std::error::Error>> {
2467        assert_eq!(
2468            Ok(SystemMessage::PermissionToRezObjectDenied {
2469                object_name: "Foo2".to_string(),
2470                attempted_rez_location: sl_types::map::RegionCoordinates::new(63.0486, 45.2515, 1501.08),
2471                parcel_name: "The Foo Bar".to_string(),
2472                region_name: sl_types::map::RegionName::try_new("Fudo")?,
2473            }),
2474            permission_to_rez_object_denied_message_parser()
2475                .parse("Can't rez object 'Foo2' at { 63.0486, 45.2515, 1501.08 } on parcel 'The Foo Bar' in region Fudo because the owner of this land does not allow it.  Use the land tool to see land ownership.").into_result()
2476        );
2477        Ok(())
2478    }
2479}