From b5aa53690c2b681851854b3afc02b00bff7318cd Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 8 Apr 2026 16:19:05 +0800 Subject: [PATCH] Add CabbageCxCrypt --- msg_tool_xp3data/crypt.json | 12 ++ msg_tool_xp3data/cx_cb/hoshikoi.bin | Bin 0 -> 4096 bytes src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 140 ++++++++++++++++++ src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 18 +++ src/scripts/kirikiri/archive/xp3/mod.rs | 4 +- 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 msg_tool_xp3data/cx_cb/hoshikoi.bin diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index bde7fef..f573fca 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -574,6 +574,18 @@ "$type": "HashCrypt", "Title": "ほら、そんなに声を漏らすと彼氏にバレちまうぜ? ~エロゲ世界でモブの少女を寝取ってみた~" }, + "Hoshi Koi * Twinkle": { + "$type": "CabbageCxCrypt", + "Mask": 701, + "Offset": 172, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "NamesSectionId": "cbg:", + "RandomSeed": 2463534242, + "ControlBlockName": "hoshikoi.bin", + "Title": "星恋*ティンクル | 星恋*twinkle | 星恋*闪烁" + }, "Ichizu ni Zettai Fukujuu na Imouto": { "$type": "HashCrypt", "Title": "一途に絶対服従な妹をめちゃくちゃにしてみた ~愛欲兄妹~" diff --git a/msg_tool_xp3data/cx_cb/hoshikoi.bin b/msg_tool_xp3data/cx_cb/hoshikoi.bin new file mode 100644 index 0000000000000000000000000000000000000000..020c1ed00cda84afb42b85acc57b556dfe849a16 GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index 7c7116d..0142552 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -925,3 +925,143 @@ impl Crypt for Arc { Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) } } + +#[derive(Debug)] +struct CxProgramNana { + base: CxProgram, + random_seed: u32, +} + +impl CxProgramNana { + fn new(seed: u32, control_blocks: Weak>, random_seed: u32) -> Self { + Self { + base: CxProgram { + code: Vec::with_capacity(CX_PROGRAM_SIZE), + control_block: control_blocks, + length: 0, + seed, + }, + random_seed, + } + } +} + +impl ICxProgram for CxProgramNana { + fn execute(&self, hash: u32) -> Result { + self.base.execute(hash) + } + fn clear(&mut self) { + self.base.clear(); + } + fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool { + self.base.emit(bytecode, length) + } + fn emit_nop(&mut self, count: usize) -> bool { + self.base.emit_nop(count) + } + fn emit_u32(&mut self, x: u32) -> bool { + self.base.emit_u32(x) + } + fn emit_random(&mut self) -> bool { + let random = self.get_random(); + self.emit_u32(random) + } + fn get_random(&mut self) -> u32 { + let mut s = self.base.seed ^ (self.base.seed << 17); + s ^= (s << 18) | (s >> 15); + self.base.seed = !s; + let mut r = self.random_seed ^ (self.random_seed << 13); + r ^= r >> 17; + self.random_seed = r ^ (r << 5); + self.base.seed ^ self.random_seed + } +} + +#[derive(Debug)] +struct CxProgramNanaBuilder { + random_seed: u32, +} + +impl CxProgramNanaBuilder { + fn new(random_seed: u32) -> Self { + Self { random_seed } + } +} + +impl ICxProgramBuilder for CxProgramNanaBuilder { + fn build(&self, seed: u32, control_blocks: Weak>) -> Box { + Box::new(CxProgramNana::new(seed, control_blocks, self.random_seed)) + } +} + +#[derive(Debug)] +pub struct CabbageCxCrypt { + base: SenrenCxCrypt, +} + +impl AsRef for CabbageCxCrypt { + fn as_ref(&self) -> &BaseSchema { + self.base.as_ref() + } +} + +impl CabbageCxCrypt { + pub fn new( + base: BaseSchema, + schema: &CxSchema, + filename: &str, + names_section_id: String, + random_seed: u32, + ) -> Result> { + Ok(Arc::new(Self { + base: SenrenCxCrypt::new_inner( + base, + schema, + filename, + Box::new(CxProgramNanaBuilder::new(random_seed)), + names_section_id, + )?, + })) + } +} + +icx_enc_impl!(CabbageCxCrypt); +icx_enc_arc_impl!(CabbageCxCrypt); + +impl Crypt for Arc { + base_schema_impl!(); + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + default_init_crypt(archive)?; + self.base.read_yuzu_names(archive) + } + fn decrypt_supported(&self) -> bool { + true + } + fn decrypt_seek_supported(&self) -> bool { + true + } + fn decrypt<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + let key = ( + entry.file_hash, + Box::new(self.clone()) as Box, + ); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + let key = ( + entry.file_hash, + Box::new(self.clone()) as Box, + ); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index f324120..b1886b2 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -130,6 +130,13 @@ enum CryptType { cx: CxSchema, names_section_id: String, }, + #[serde(rename_all = "PascalCase")] + CabbageCxCrypt { + #[serde(flatten)] + cx: CxSchema, + names_section_id: String, + random_seed: u32, + }, } #[derive(Clone, Debug, Deserialize)] @@ -174,6 +181,17 @@ impl Schema { filename, names_section_id.clone(), )?), + CryptType::CabbageCxCrypt { + cx, + names_section_id, + random_seed, + } => Box::new(cx::CabbageCxCrypt::new( + self.base.clone(), + cx, + filename, + names_section_id.clone(), + *random_seed, + )?), }) } } diff --git a/src/scripts/kirikiri/archive/xp3/mod.rs b/src/scripts/kirikiri/archive/xp3/mod.rs index 374053d..18bdc5d 100644 --- a/src/scripts/kirikiri/archive/xp3/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -18,7 +18,7 @@ pub use crypt::get_supported_games_with_title; use flate2::read::ZlibDecoder; use overf::wrapping; pub use segmenter::SegmenterConfig; -use std::io::{Read, Seek, SeekFrom}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::sync::{Arc, Mutex}; use writer::Xp3ArchiveWriter; use zstd::stream::read::Decoder as ZstdDecoder; @@ -174,6 +174,8 @@ impl Xp3Archive { let mut archive = archive::Xp3Archive::new(stream, config, filename)?; if config.xp3_debug_archive { println!("Debug info for {}:\n{:#?}", filename, archive); + // Try flush stdout. + let _ = std::io::stdout().flush(); } archive.entries.retain(|entry| { let i = &entry.name;