From ecda1cc1266117b3bb8669b06185d2e15a265ebe Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Tue, 2 Apr 2019 11:59:58 +0200 Subject: [PATCH] game: add Discworld Noir BMV demuxer and audio decoder --- nihav-core/src/detect.rs | 6 + nihav-core/src/register.rs | 2 + nihav-game/Cargo.toml | 6 +- nihav-game/src/codecs/bmv3.rs | 211 +++++++++++++++++++++++++++++++++ nihav-game/src/codecs/mod.rs | 6 + nihav-game/src/demuxers/bmv.rs | 157 ++++++++++++++++++++++++ nihav-game/src/demuxers/mod.rs | 4 +- 7 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 nihav-game/src/codecs/bmv3.rs diff --git a/nihav-core/src/detect.rs b/nihav-core/src/detect.rs index 17b1411..caf5900 100644 --- a/nihav-core/src/detect.rs +++ b/nihav-core/src/detect.rs @@ -211,6 +211,12 @@ const DETECTORS: &[DetectConditions] = &[ extensions: ".bmv", conditions: &[], }, + DetectConditions { + demux_name: "bmv3", + extensions: ".bmv", + conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"BMVi") }, + CheckItem{offs: 32, cond: &CC::Str(b"DATA")}], + }, DetectConditions { demux_name: "vmd", extensions: ".vmd", diff --git a/nihav-core/src/register.rs b/nihav-core/src/register.rs index 56d2cfc..a03d7ca 100644 --- a/nihav-core/src/register.rs +++ b/nihav-core/src/register.rs @@ -165,6 +165,8 @@ static CODEC_REGISTER: &'static [CodecDescription] = &[ desc!(audio; "gdv-audio", "Gremlin Digital Video - audio"), desc!(video; "bmv-video", "BMV video"), desc!(audio; "bmv-audio", "BMV audio"), + desc!(video; "bmv3-video", "DW Noir BMV video"), + desc!(audio; "bmv3-audio", "DW Noir BMV audio"), desc!(video; "vmd-video", "VMD video"), desc!(audio; "vmd-audio", "VMD audio"), diff --git a/nihav-game/Cargo.toml b/nihav-game/Cargo.toml index 1da5704..3907a6f 100644 --- a/nihav-game/Cargo.toml +++ b/nihav-game/Cargo.toml @@ -11,16 +11,18 @@ features = [] [features] default = ["all_decoders", "all_demuxers"] demuxers = [] -all_demuxers = ["demuxer_bmv", "demuxer_gdv", "demuxer_vmd"] +all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_gdv", "demuxer_vmd"] demuxer_bmv = ["demuxers"] +demuxer_bmv3 = ["demuxers"] demuxer_gdv = ["demuxers"] demuxer_vmd = ["demuxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] decoders = [] -all_video_decoders = ["decoder_bmv", "decoder_gdvvid", "decoder_vmd"] +all_video_decoders = ["decoder_bmv", "decoder_bmv3", "decoder_gdvvid", "decoder_vmd"] decoder_bmv = ["decoders"] +decoder_bmv3 = ["decoders"] decoder_gdvvid = ["decoders"] decoder_vmd = ["decoders"] diff --git a/nihav-game/src/codecs/bmv3.rs b/nihav-game/src/codecs/bmv3.rs new file mode 100644 index 0000000..2edd8c2 --- /dev/null +++ b/nihav-game/src/codecs/bmv3.rs @@ -0,0 +1,211 @@ +use nihav_core::formats; +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use std::str::FromStr; + + +pub fn get_decoder_video() -> Box { + unimplemented!(); +} + +struct BMV3AudioDecoder { + ainfo: NAAudioInfo, + chmap: NAChannelMap, + pred: [i16; 2], + nframes: usize, +} + +impl BMV3AudioDecoder { + fn new() -> Self { + Self { + ainfo: NAAudioInfo::new(0, 1, formats::SND_S16P_FORMAT, 0), + chmap: NAChannelMap::new(), + pred: [0; 2], + nframes: 0, + } + } +} + +fn decode_block(mode: u8, src: &[u8], dst: &mut [i16], mut pred: i16) -> i16 { + let steps = &BMV_AUDIO_STEPS[mode as usize]; + let mut val2 = 0; + for i in 0..10 { + let val = (src[i * 2 + 0] as usize) + (src[i * 2 + 1] as usize) * 256; + pred = pred.wrapping_add(steps[(val >> 10) & 0x1F]); + dst[i * 3 + 0] = pred; + pred = pred.wrapping_add(steps[(val >> 5) & 0x1F]); + dst[i * 3 + 1] = pred; + pred = pred.wrapping_add(steps[(val >> 0) & 0x1F]); + dst[i * 3 + 2] = pred; + val2 = (val2 << 1) | (val >> 15); + } + pred = pred.wrapping_add(steps[(val2 >> 5) & 0x1F]); + dst[3 * 10 + 0] = pred; + pred = pred.wrapping_add(steps[(val2 >> 0) & 0x1F]); + dst[3 * 10 + 1] = pred; + pred +} + +impl NADecoder for BMV3AudioDecoder { + fn init(&mut self, info: Rc) -> DecoderResult<()> { + if let NACodecTypeInfo::Audio(ainfo) = info.get_properties() { + self.ainfo = NAAudioInfo::new(ainfo.get_sample_rate(), ainfo.get_channels(), formats::SND_S16P_FORMAT, 32); + self.chmap = NAChannelMap::from_str("L,R").unwrap(); + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, pkt: &NAPacket) -> DecoderResult { + let info = pkt.get_stream().get_info(); + if let NACodecTypeInfo::Audio(_) = info.get_properties() { + let pktbuf = pkt.get_buffer(); + validate!(pktbuf.len() > 1); + let samples = (pktbuf.len() / 41) * 32; + let abuf = alloc_audio_buffer(self.ainfo, samples, self.chmap.clone())?; + let mut adata = abuf.get_abuf_i16().unwrap(); + let off1 = adata.get_offset(1); + let mut dst = adata.get_data_mut(); + let mut first = pktbuf[0] == 0; + let psrc = &pktbuf[1..]; + for (n, src) in psrc.chunks_exact(41).enumerate() { + let aoff0 = n * 32; + let aoff1 = aoff0 + off1; + if first { + let mode = src[40]; + self.pred[0] = decode_block(mode >> 4, &src[0..], &mut dst[aoff0..], self.pred[0]); + self.pred[1] = decode_block(mode & 0xF, &src[20..], &mut dst[aoff1..], self.pred[1]); + } else { + let mode = src[0]; + self.pred[0] = decode_block(mode >> 4, &src[1..], &mut dst[aoff0..], self.pred[0]); + self.pred[1] = decode_block(mode & 0xF, &src[21..], &mut dst[aoff1..], self.pred[1]); + } + first = !first; + } + self.nframes += 1; + let mut frm = NAFrame::new_from_pkt(pkt, info, abuf); + frm.set_duration(Some(samples as u64)); + frm.set_keyframe(false); + Ok(Rc::new(RefCell::new(frm))) + } else { + Err(DecoderError::InvalidData) + } + } +} + +pub fn get_decoder_audio() -> Box { + Box::new(BMV3AudioDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_core::test::dec_video::*; + use crate::codecs::game_register_all_codecs; + use crate::demuxers::game_register_all_demuxers; + #[test] + fn test_bmv_video() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_codecs(&mut dec_reg); + + let file = "assets/Game/DW3-Loffnote.bmv"; + test_file_decoding("bmv3", file, Some(40), true, false, None, &dmx_reg, &dec_reg); + } + #[test] + fn test_bmv_audio() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_codecs(&mut dec_reg); + + let file = "assets/Game/DW3-Loffnote.bmv"; + test_decode_audio("bmv3", file, None, "bmv3", &dmx_reg, &dec_reg); + } +} + +const BMV_AUDIO_STEPS: [[i16; 32]; 16] = [ + [ + 0x0000, 0x0400, 0x0800, 0x0C00, 0x1000, 0x1400, 0x1800, 0x1C00, + 0x2000, 0x2400, 0x2800, 0x2C00, 0x3000, 0x3400, 0x3800, 0x3C00, + -0x4000, -0x3C00, -0x3800, -0x3400, -0x3000, -0x2C00, -0x2800, -0x2400, + -0x2000, -0x1C00, -0x1800, -0x1400, -0x1000, -0x0C00, -0x0800, -0x0400 + ], [ + 0x0000, 0x0200, 0x0400, 0x0600, 0x0800, 0x0A00, 0x0C00, 0x0E00, + 0x1000, 0x1200, 0x1400, 0x1600, 0x1800, 0x1A00, 0x1C00, 0x1E00, + -0x2000, -0x1E00, -0x1C00, -0x1A00, -0x1800, -0x1600, -0x1400, -0x1200, + -0x1000, -0x0E00, -0x0C00, -0x0A00, -0x0800, -0x0600, -0x0400, -0x0200 + ], [ + 0x0000, 0x0100, 0x0200, 0x0300, 0x0400, 0x0500, 0x0600, 0x0700, + 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00, 0x0F00, + -0x1000, -0x0F00, -0x0E00, -0x0D00, -0x0C00, -0x0B00, -0x0A00, -0x0900, + -0x0800, -0x0700, -0x0600, -0x0500, -0x0400, -0x0300, -0x0200, -0x0100 + ], [ + 0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, + 0x400, 0x480, 0x500, 0x580, 0x600, 0x680, 0x700, 0x780, + -0x800, -0x780, -0x700, -0x680, -0x600, -0x580, -0x500, -0x480, + -0x400, -0x380, -0x300, -0x280, -0x200, -0x180, -0x100, -0x080 + ], [ + 0x000, 0x048, 0x090, 0x0D8, 0x120, 0x168, 0x1B0, 0x1F8, + 0x240, 0x288, 0x2D0, 0x318, 0x360, 0x3A8, 0x3F0, 0x438, + -0x480, -0x438, -0x3F0, -0x3A8, -0x360, -0x318, -0x2D0, -0x288, + -0x240, -0x1F8, -0x1B0, -0x168, -0x120, -0x0D8, -0x090, -0x048 + ], [ + 0x000, 0x030, 0x060, 0x090, 0x0C0, 0x0F0, 0x120, 0x150, + 0x180, 0x1B0, 0x1E0, 0x210, 0x240, 0x270, 0x2A0, 0x2D0, + -0x300, -0x2D0, -0x2A0, -0x270, -0x240, -0x210, -0x1E0, -0x1B0, + -0x180, -0x150, -0x120, -0x0F0, -0x0C0, -0x090, -0x060, -0x030 + ], [ + 0x000, 0x020, 0x040, 0x060, 0x080, 0x0A0, 0x0C0, 0x0E0, + 0x100, 0x120, 0x140, 0x160, 0x180, 0x1A0, 0x1C0, 0x1E0, + -0x200, -0x1E0, -0x1C0, -0x1A0, -0x180, -0x160, -0x140, -0x120, + -0x100, -0x0E0, -0x0C0, -0x0A0, -0x080, -0x060, -0x040, -0x020 + ], [ + 0x000, 0x016, 0x02C, 0x042, 0x058, 0x06E, 0x084, 0x09A, + 0x0B0, 0x0C6, 0x0DC, 0x0F2, 0x108, 0x11E, 0x134, 0x14A, + -0x160, -0x14A, -0x134, -0x11E, -0x108, -0x0F2, -0x0DC, -0x0C6, + -0x0B0, -0x09A, -0x084, -0x06E, -0x058, -0x042, -0x02C, -0x016 + ], [ + 0x000, 0x010, 0x020, 0x030, 0x040, 0x050, 0x060, 0x070, + 0x080, 0x090, 0x0A0, 0x0B0, 0x0C0, 0x0D0, 0x0E0, 0x0F0, + -0x100, -0x0F0, -0x0E0, -0x0D0, -0x0C0, -0x0B0, -0x0A0, -0x090, + -0x080, -0x070, -0x060, -0x050, -0x040, -0x030, -0x020, -0x010 + ], [ + 0x00, 0x0B, 0x16, 0x21, 0x2C, 0x37, 0x42, 0x4D, + 0x58, 0x63, 0x6E, 0x79, 0x84, 0x8F, 0x9A, 0xA5, + -0xB0, -0xA5, -0x9A, -0x8F, -0x84, -0x79, -0x6E, -0x63, + -0x58, -0x4D, -0x42, -0x37, -0x2C, -0x21, -0x16, -0x0B + ], [ + 0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, + 0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, 0x78, + -0x80, -0x78, -0x70, -0x68, -0x60, -0x58, -0x50, -0x48, + -0x40, -0x38, -0x30, -0x28, -0x20, -0x18, -0x10, -0x08 + ], [ + 0x00, 0x06, 0x0C, 0x12, 0x18, 0x1E, 0x24, 0x2A, + 0x30, 0x36, 0x3C, 0x42, 0x48, 0x4E, 0x54, 0x5A, + -0x60, -0x5A, -0x54, -0x4E, -0x48, -0x42, -0x3C, -0x36, + -0x30, -0x2A, -0x24, -0x1E, -0x18, -0x12, -0x0C, -0x06 + ], [ + 0x00, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C, + 0x20, 0x24, 0x28, 0x2C, 0x30, 0x34, 0x38, 0x3C, + -0x40, -0x3C, -0x38, -0x34, -0x30, -0x2C, -0x28, -0x24, + -0x20, -0x1C, -0x18, -0x14, -0x10, -0x0C, -0x08, -0x04 + ], [ + 0x00, 0x02, 0x05, 0x08, 0x0B, 0x0D, 0x10, 0x13, + 0x16, 0x18, 0x1B, 0x1E, 0x21, 0x23, 0x26, 0x29, + -0x2C, -0x2A, -0x27, -0x24, -0x21, -0x1F, -0x1C, -0x19, + -0x16, -0x14, -0x11, -0x0E, -0x0B, -0x09, -0x06, -0x03 + ], [ + 0x00, 0x01, 0x03, 0x05, 0x07, 0x08, 0x0A, 0x0C, + 0x0E, 0x0F, 0x11, 0x13, 0x15, 0x16, 0x18, 0x1A, + -0x1C, -0x1B, -0x19, -0x17, -0x15, -0x14, -0x12, -0x10, + -0x0E, -0x0D, -0x0B, -0x09, -0x07, -0x06, -0x04, -0x02 + ], [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + -0x10, -0x0F, -0x0E, -0x0D, -0x0C, -0x0B, -0x0A, -0x09, + -0x08, -0x07, -0x06, -0x05, -0x04, -0x03, -0x02, -0x01 + ] +]; diff --git a/nihav-game/src/codecs/mod.rs b/nihav-game/src/codecs/mod.rs index fde6601..0cd5f35 100644 --- a/nihav-game/src/codecs/mod.rs +++ b/nihav-game/src/codecs/mod.rs @@ -6,6 +6,8 @@ macro_rules! validate { #[cfg(feature="decoder_bmv")] pub mod bmv; +#[cfg(feature="decoder_bmv3")] +pub mod bmv3; #[cfg(feature="decoder_gdvvid")] pub mod gremlinvideo; #[cfg(feature="decoder_vmd")] @@ -20,6 +22,10 @@ const GAME_CODECS: &[DecoderInfo] = &[ DecoderInfo { name: "bmv-audio", get_decoder: bmv::get_decoder_audio }, #[cfg(feature="decoder_bmv")] DecoderInfo { name: "bmv-video", get_decoder: bmv::get_decoder_video }, +#[cfg(feature="decoder_bmv3")] + DecoderInfo { name: "bmv3-audio", get_decoder: bmv3::get_decoder_audio }, +#[cfg(feature="decoder_bmv3")] + DecoderInfo { name: "bmv3-video", get_decoder: bmv3::get_decoder_video }, #[cfg(feature="decoder_vmd")] DecoderInfo { name: "vmd-audio", get_decoder: vmd::get_decoder_audio }, #[cfg(feature="decoder_vmd")] diff --git a/nihav-game/src/demuxers/bmv.rs b/nihav-game/src/demuxers/bmv.rs index 8baa6e4..7553a94 100644 --- a/nihav-game/src/demuxers/bmv.rs +++ b/nihav-game/src/demuxers/bmv.rs @@ -99,6 +99,145 @@ impl DemuxerCreator for BMVDemuxerCreator { fn get_name(&self) -> &'static str { "bmv" } } +struct BMV3Demuxer<'a> { + src: &'a mut ByteReader<'a>, + vid_id: usize, + aud_id: usize, + vpos: u64, + apos: u64, + asize: usize, + ablob: usize, + pkt_buf: Vec, +} + +impl<'a> DemuxCore<'a> for BMV3Demuxer<'a> { + #[allow(unused_variables)] + fn open(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<()> { + let src = &mut self.src; + + let mut magic = [0u8; 4]; + src.read_buf(&mut magic)?; + validate!(&magic[0..] == b"BMVi"); + let size = src.read_u32le()?; + validate!(size == 24); + let _slot_size = src.read_u32le()? as usize; + let nframes = src.read_u32le()? as usize; + let _prefetch_slots = src.read_u16le()?; + let _cache_size = src.read_u16le()?; + let fps = src.read_u16le()? as u32; + let audio_size = src.read_u16le()? as usize; + let audio_blob_size = src.read_u16le()? as usize; + let _audio_id = src.read_byte()?; + let _video_id = src.read_byte()?; + let width = src.read_u16le()? as usize; + let height = src.read_u16le()? as usize; + validate!(nframes > 0); + validate!((width > 0) && (width <= 640)); + validate!((height > 0) && (height <= 432)); + validate!((audio_size > audio_blob_size) && (audio_blob_size > 0) && (audio_size % audio_blob_size == 0)); + let mut dta = [0u8; 4]; + src.read_buf(&mut dta)?; + validate!(&dta[0..] == b"DATA"); + let data_size = src.read_u32le()? as usize; + validate!(data_size > 0); + self.asize = audio_size; + self.ablob = audio_blob_size; + + let vhdr = NAVideoInfo::new(width, height, false, RGB565_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("bmv3-video", vci, None); + self.vid_id = strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 256, fps)).unwrap(); + + let ahdr = NAAudioInfo::new(22050, 2, SND_S16_FORMAT, audio_blob_size); + let ainfo = NACodecInfo::new("bmv3-audio", NACodecTypeInfo::Audio(ahdr), None); + self.aud_id = strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, 22050)).unwrap(); + + self.vpos = 0; + self.apos = 0; + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.pkt_buf.len() > 0 { + return Ok(self.pkt_buf.pop().unwrap()); + } + + loop { + let ctype = self.src.read_byte()?; + if ctype == 0 { // NOP chunk + continue; + } + if ctype == 1 { return Err(DemuxerError::EOF); } + let size = self.src.read_u24le()? as usize; + if size == 0 { continue; } + let asize; + if (ctype & 0x20) != 0 { + if (ctype & 0x40) != 0 { + asize = self.asize - self.ablob; + } else { + asize = self.asize; + } + validate!(asize <= size); + let mut buf: Vec = Vec::with_capacity(asize + 1); + buf.resize(asize + 1, 0); + buf[0] = (self.src.tell() & 1) as u8; + self.src.read_buf(&mut buf[1..])?; + + let str = strmgr.get_stream(self.aud_id).unwrap(); + let (tb_num, tb_den) = str.get_timebase(); + let ts = NATimeInfo::new(Some(self.apos), None, None, tb_num, tb_den); + let apkt = NAPacket::new(str, ts, false, buf); + + self.apos += (asize as u64) / 41 * 32; + self.pkt_buf.push(apkt); + } else { + asize = 0; + } + let mut buf: Vec = Vec::with_capacity(size - asize + 1); + buf.resize(size - asize + 1, 0); + buf[0] = ctype; + self.src.read_buf(&mut buf[1..])?; + + let str = strmgr.get_stream(self.vid_id).unwrap(); + let (tb_num, tb_den) = str.get_timebase(); + let ts = NATimeInfo::new(Some(self.vpos), None, None, tb_num, tb_den); + let pkt = NAPacket::new(str, ts, (ctype & 3) == 3, buf); + + self.vpos += 1; + return Ok(pkt); + } + } + + #[allow(unused_variables)] + fn seek(&mut self, time: u64) -> DemuxerResult<()> { + Err(DemuxerError::NotImplemented) + } +} + +impl<'a> BMV3Demuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + Self { + src: io, + vid_id: 0, + aud_id: 0, + vpos: 0, + apos: 0, + asize: 0, + ablob: 0, + pkt_buf: Vec::with_capacity(1), + } + } +} + +pub struct BMV3DemuxerCreator { } + +impl DemuxerCreator for BMV3DemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(BMV3Demuxer::new(br)) + } + fn get_name(&self) -> &'static str { "bmv3" } +} + #[cfg(test)] mod test { use super::*; @@ -122,4 +261,22 @@ mod test { println!("Got {}", pkt); } } + #[test] + fn test_bmv3_demux() { + let mut file = File::open("assets/Game/DW3-Loffnote.bmv").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = BMV3Demuxer::new(&mut br); + let mut sm = StreamManager::new(); + dmx.open(&mut sm).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } } diff --git a/nihav-game/src/demuxers/mod.rs b/nihav-game/src/demuxers/mod.rs index 9d7d187..884539c 100644 --- a/nihav-game/src/demuxers/mod.rs +++ b/nihav-game/src/demuxers/mod.rs @@ -5,7 +5,7 @@ macro_rules! validate { ($a:expr) => { if !$a { println!("check failed at {}:{}", file!(), line!()); return Err(DemuxerError::InvalidData); } }; } -#[cfg(feature="demuxer_bmv")] +#[cfg(any(feature="demuxer_bmv",feature="demuxer_bmv3"))] mod bmv; #[cfg(feature="demuxer_gdv")] mod gdv; @@ -15,6 +15,8 @@ mod vmd; const GAME_DEMUXERS: &[&'static DemuxerCreator] = &[ #[cfg(feature="demuxer_bmv")] &bmv::BMVDemuxerCreator {}, +#[cfg(feature="demuxer_bmv3")] + &bmv::BMV3DemuxerCreator {}, #[cfg(feature="demuxer_gdv")] &gdv::GDVDemuxerCreator {}, #[cfg(feature="demuxer_vmd")] -- 2.30.2