boon-deadlock 0.1.0

Boon is a Deadlock demo / replay file parser
Documentation
use prost::Message;

/// Helper macro: expands to a `match` that tries `prost::Message::decode`
/// for each `(msg_type => ProtoType)` pair and pretty-prints the result.
macro_rules! decode_match {
    ($msg_type:expr, $data:expr, $($val:expr => $ty:ty),* $(,)?) => {
        match $msg_type {
            $($val => <$ty>::decode($data).ok().map(|m| format!("{:#?}", m)),)*
            _ => None,
        }
    }
}

/// Attempt to decode raw protobuf bytes for a known event message type.
///
/// Returns `Some(pretty-printed string)` if the type is recognized and the
/// payload decodes successfully, or `None` otherwise.
pub fn decode_event_payload(msg_type: u32, data: &[u8]) -> Option<String> {
    use boon_proto::proto::*;

    decode_match!(msg_type, data,
        // CitadelUserMessageIds (300–366)
        300 => CCitadelUserMessageDamage,
        303 => CCitadelUserMsgMapPing,
        304 => CCitadelUserMsgTeamRewards,
        308 => CCitadelUserMsgTriggerDamageFlash,
        309 => CCitadelUserMsgAbilitiesChanged,
        310 => CCitadelUserMsgRecentDamageSummary,
        311 => CCitadelUserMsgSpectatorTeamChanged,
        312 => CCitadelUserMsgChatWheel,
        313 => CCitadelUserMsgGoldHistory,
        314 => CCitadelUserMsgChatMsg,
        315 => CCitadelUserMsgQuickResponse,
        316 => CCitadelUserMsgPostMatchDetails,
        317 => CCitadelUserMsgChatEvent,
        318 => CCitadelUserMsgAbilityInterrupted,
        319 => CCitadelUserMsgHeroKilled,
        320 => CCitadelUserMsgReturnIdol,
        321 => CCitadelUserMsgSetClientCameraAngles,
        322 => CCitadelUserMsgMapLine,
        323 => CCitadelUserMessageBulletHit,
        324 => CCitadelUserMessageObjectiveMask,
        325 => CCitadelUserMessageModifierApplied,
        326 => CCitadelUserMsgCameraController,
        327 => CCitadelUserMessageAuraModifierApplied,
        329 => CCitadelUserMsgObstructedShotFired,
        330 => CCitadelUserMsgAbilityLateFailure,
        331 => CCitadelUserMsgAbilityPing,
        332 => CCitadelUserMsgPostProcessingAnim,
        333 => CCitadelUserMsgDeathReplayData,
        334 => CCitadelUserMsgPlayerLifetimeStatInfo,
        336 => CCitadelUserMsgForceShopClosed,
        337 => CCitadelUserMsgStaminaConsumed,
        338 => CCitadelUserMessageAbilityNotify,
        339 => CCitadelUserMsgGetDamageStatsResponse,
        340 => CCitadelUserMsgParticipantStartSoundEvent,
        341 => CCitadelUserMsgParticipantStopSoundEvent,
        342 => CCitadelUserMsgParticipantStopSoundEventHash,
        343 => CCitadelUserMsgParticipantSetSoundEventParams,
        344 => CCitadelUserMsgParticipantSetLibraryStackFields,
        345 => CCitadelUserMessageCurrencyChanged,
        346 => CCitadelUserMessageGameOver,
        347 => CCitadelUserMsgBossKilled,
        348 => CCitadelUserMsgBossDamaged,
        349 => CCitadelUserMsgMidBossSpawned,
        350 => CCitadelUserMsgRejuvStatus,
        351 => CCitadelUserMsgKillStreak,
        352 => CCitadelUserMsgTeamMsg,
        353 => CCitadelUserMsgPlayerRespawned,
        354 => CCitadelUserMsgCallCheaterVote,
        355 => CCitadelUserMessageMeleeHit,
        356 => CCitadelUserMsgFlexSlotUnlocked,
        357 => CCitadelUserMsgSeasonalKill,
        358 => CCitadelUserMsgMusicQueue,
        359 => CCitadelUserMsgAg2ParamTrigger,
        360 => CCitadelUserMessageItemPurchaseNotification,
        361 => CCitadelUserMsgEntityPortalled,
        362 => CCitadelUserMsgStreetBrawlScoring,
        363 => CCitadelUserMsgHudGameAnnouncement,
        364 => CCitadelUserMsgItemDraftReaction,
        365 => CCitadelUserMessageImportantAbilityUsed,
        366 => CCitadelUserMsgBannedHeroes,

        // ECitadelGameEvents (450–466)
        450 => CMsgFireBullets,
        451 => CMsgPlayerAnimEvent,
        458 => CMsgParticleSystemManager,
        459 => CMsgScreenTextPretty,
        461 => CMsgBulletImpact,
        462 => CMsgEnableSatVolumesEvent,
        463 => CMsgPlaceSatVolumeEvent,
        464 => CMsgDisableSatVolumesEvent,
        465 => CMsgRemoveSatVolumeEvent,
        466 => CMsgRemoveBullet,

        // EBaseUserMessages (101-166)
        101 => CUserMessageAchievementEvent,
        102 => CUserMessageCloseCaption,
        103 => CUserMessageCloseCaptionDirect,
        104 => CUserMessageCurrentTimescale,
        105 => CUserMessageDesiredTimescale,
        106 => CUserMessageFade,
        107 => CUserMessageGameTitle,
        110 => CUserMessageHudMsg,
        111 => CUserMessageHudText,
        113 => CUserMessageColoredText,
        114 => CUserMessageRequestState,
        115 => CUserMessageResetHud,
        116 => CUserMessageRumble,
        117 => CUserMessageSayText,
        118 => CUserMessageSayText2,
        119 => CUserMessageSayTextChannel,
        120 => CUserMessageShake,
        121 => CUserMessageShakeDir,
        122 => CUserMessageWaterShake,
        124 => CUserMessageTextMsg,
        125 => CUserMessageScreenTilt,
        128 => CUserMessageVoiceMask,
        130 => CUserMessageSendAudio,
        131 => CUserMessageItemPickup,
        132 => CUserMessageAmmoDenied,
        134 => CUserMessageShowMenu,
        135 => CUserMessageCreditsMsg,
        142 => CUserMessageCloseCaptionPlaceholder,
        143 => CUserMessageCameraTransition,
        144 => CUserMessageAudioParameter,
        145 => CUserMsgParticleManager,
        146 => CUserMsgHudError,
        148 => CUserMsgCustomGameEvent,
        149 => CUserMessageAnimStateGraphState,
        150 => CUserMessageHapticsManagerPulse,
        151 => CUserMessageHapticsManagerEffect,
        153 => CUserMessageUpdateCssClasses,
        154 => CUserMessageServerFrameTime,
        155 => CUserMessageLagCompensationError,
        156 => CUserMessageRequestDllStatus,
        157 => CUserMessageRequestUtilAction,
        158 => CUserMessageUtilMsgResponse,
        159 => CUserMessageDllStatus,
        160 => CUserMessageRequestInventory,
        161 => CUserMessageInventoryResponse,
        162 => CUserMessageRequestDiagnostic,
        163 => CUserMessageDiagnosticResponse,
        164 => CUserMessageExtraUserData,
        165 => CUserMessageNotifyResponseFound,
        166 => CUserMessagePlayResponseConditional,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn unknown_msg_type_returns_none() {
        assert!(decode_event_payload(0, &[]).is_none());
        assert!(decode_event_payload(9999, &[1, 2, 3]).is_none());
    }

    #[test]
    fn known_type_invalid_bytes_returns_none() {
        // 300 is a known type (CCitadelUserMessageDamage), but garbage bytes
        // may or may not decode (protobuf is lenient), so we just verify no panic
        let result = decode_event_payload(300, &[0xFF, 0xFF, 0xFF, 0xFF]);
        // Result can be Some or None depending on protobuf leniency - just ensure no panic
        let _ = result;
    }

    #[test]
    fn known_type_empty_bytes_returns_some() {
        // Empty bytes should decode as an empty protobuf message
        let result = decode_event_payload(300, &[]);
        assert!(result.is_some());
    }
}