use core::fmt;
pub const UOP_TIME_PLAY_OR_SEARCH: u8 = 0;
pub const UOP_PTT_PLAY_OR_SEARCH: u8 = 1;
pub const UOP_TITLE_PLAY: u8 = 2;
pub const UOP_STOP: u8 = 3;
pub const UOP_GO_UP: u8 = 4;
pub const UOP_TIME_OR_PTT_SEARCH: u8 = 5;
pub const UOP_TOP_PG_OR_PREV_PG_SEARCH: u8 = 6;
pub const UOP_NEXT_PG_SEARCH: u8 = 7;
pub const UOP_FORWARD_SCAN: u8 = 8;
pub const UOP_BACKWARD_SCAN: u8 = 9;
pub const UOP_MENU_CALL_TITLE: u8 = 10;
pub const UOP_MENU_CALL_ROOT: u8 = 11;
pub const UOP_MENU_CALL_SUBPICTURE: u8 = 12;
pub const UOP_MENU_CALL_AUDIO: u8 = 13;
pub const UOP_MENU_CALL_ANGLE: u8 = 14;
pub const UOP_MENU_CALL_PTT: u8 = 15;
pub const UOP_RESUME: u8 = 16;
pub const UOP_BUTTON_SELECT_OR_ACTIVATE: u8 = 17;
pub const UOP_STILL_OFF: u8 = 18;
pub const UOP_PAUSE_ON: u8 = 19;
pub const UOP_AUDIO_STREAM_CHANGE: u8 = 20;
pub const UOP_SUBPICTURE_STREAM_CHANGE: u8 = 21;
pub const UOP_ANGLE_CHANGE: u8 = 22;
pub const UOP_KARAOKE_AUDIO_MIX_CHANGE: u8 = 23;
pub const UOP_VIDEO_PRESENTATION_MODE_CHANGE: u8 = 24;
pub const UOP_BIT_COUNT: u8 = 25;
pub const UOP_DEFINED_BITS: u32 = (1u32 << UOP_BIT_COUNT) - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum UserOp {
TimePlayOrSearch = UOP_TIME_PLAY_OR_SEARCH,
PttPlayOrSearch = UOP_PTT_PLAY_OR_SEARCH,
TitlePlay = UOP_TITLE_PLAY,
Stop = UOP_STOP,
GoUp = UOP_GO_UP,
TimeOrPttSearch = UOP_TIME_OR_PTT_SEARCH,
TopPgOrPrevPgSearch = UOP_TOP_PG_OR_PREV_PG_SEARCH,
NextPgSearch = UOP_NEXT_PG_SEARCH,
ForwardScan = UOP_FORWARD_SCAN,
BackwardScan = UOP_BACKWARD_SCAN,
MenuCallTitle = UOP_MENU_CALL_TITLE,
MenuCallRoot = UOP_MENU_CALL_ROOT,
MenuCallSubpicture = UOP_MENU_CALL_SUBPICTURE,
MenuCallAudio = UOP_MENU_CALL_AUDIO,
MenuCallAngle = UOP_MENU_CALL_ANGLE,
MenuCallPtt = UOP_MENU_CALL_PTT,
Resume = UOP_RESUME,
ButtonSelectOrActivate = UOP_BUTTON_SELECT_OR_ACTIVATE,
StillOff = UOP_STILL_OFF,
PauseOn = UOP_PAUSE_ON,
AudioStreamChange = UOP_AUDIO_STREAM_CHANGE,
SubpictureStreamChange = UOP_SUBPICTURE_STREAM_CHANGE,
AngleChange = UOP_ANGLE_CHANGE,
KaraokeAudioMixChange = UOP_KARAOKE_AUDIO_MIX_CHANGE,
VideoPresentationModeChange = UOP_VIDEO_PRESENTATION_MODE_CHANGE,
}
impl UserOp {
#[inline]
pub const fn bit(self) -> u8 {
self as u8
}
#[inline]
pub const fn mask(self) -> u32 {
1u32 << (self as u8)
}
pub const ALL: [UserOp; 25] = [
UserOp::TimePlayOrSearch,
UserOp::PttPlayOrSearch,
UserOp::TitlePlay,
UserOp::Stop,
UserOp::GoUp,
UserOp::TimeOrPttSearch,
UserOp::TopPgOrPrevPgSearch,
UserOp::NextPgSearch,
UserOp::ForwardScan,
UserOp::BackwardScan,
UserOp::MenuCallTitle,
UserOp::MenuCallRoot,
UserOp::MenuCallSubpicture,
UserOp::MenuCallAudio,
UserOp::MenuCallAngle,
UserOp::MenuCallPtt,
UserOp::Resume,
UserOp::ButtonSelectOrActivate,
UserOp::StillOff,
UserOp::PauseOn,
UserOp::AudioStreamChange,
UserOp::SubpictureStreamChange,
UserOp::AngleChange,
UserOp::KaraokeAudioMixChange,
UserOp::VideoPresentationModeChange,
];
pub const fn from_bit(bit: u8) -> Option<UserOp> {
match bit {
0 => Some(UserOp::TimePlayOrSearch),
1 => Some(UserOp::PttPlayOrSearch),
2 => Some(UserOp::TitlePlay),
3 => Some(UserOp::Stop),
4 => Some(UserOp::GoUp),
5 => Some(UserOp::TimeOrPttSearch),
6 => Some(UserOp::TopPgOrPrevPgSearch),
7 => Some(UserOp::NextPgSearch),
8 => Some(UserOp::ForwardScan),
9 => Some(UserOp::BackwardScan),
10 => Some(UserOp::MenuCallTitle),
11 => Some(UserOp::MenuCallRoot),
12 => Some(UserOp::MenuCallSubpicture),
13 => Some(UserOp::MenuCallAudio),
14 => Some(UserOp::MenuCallAngle),
15 => Some(UserOp::MenuCallPtt),
16 => Some(UserOp::Resume),
17 => Some(UserOp::ButtonSelectOrActivate),
18 => Some(UserOp::StillOff),
19 => Some(UserOp::PauseOn),
20 => Some(UserOp::AudioStreamChange),
21 => Some(UserOp::SubpictureStreamChange),
22 => Some(UserOp::AngleChange),
23 => Some(UserOp::KaraokeAudioMixChange),
24 => Some(UserOp::VideoPresentationModeChange),
_ => None,
}
}
pub const fn applies_to(self, level: UopLevel) -> bool {
match level {
UopLevel::TitleSearchPointer => {
matches!(self, UserOp::TimePlayOrSearch | UserOp::PttPlayOrSearch)
}
UopLevel::ProgramChain => !matches!(self, UserOp::GoUp),
UopLevel::Vobu => !matches!(
self,
UserOp::TimePlayOrSearch
| UserOp::PttPlayOrSearch
| UserOp::TitlePlay
| UserOp::ButtonSelectOrActivate
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UopLevel {
TitleSearchPointer,
ProgramChain,
Vobu,
}
impl UopLevel {
pub const fn cover(self) -> u32 {
match self {
UopLevel::TitleSearchPointer => {
(1u32 << UOP_TIME_PLAY_OR_SEARCH) | (1u32 << UOP_PTT_PLAY_OR_SEARCH)
}
UopLevel::ProgramChain => UOP_DEFINED_BITS & !(1u32 << UOP_GO_UP),
UopLevel::Vobu => {
UOP_DEFINED_BITS
& !((1u32 << UOP_TIME_PLAY_OR_SEARCH)
| (1u32 << UOP_PTT_PLAY_OR_SEARCH)
| (1u32 << UOP_TITLE_PLAY)
| (1u32 << UOP_BUTTON_SELECT_OR_ACTIVATE))
}
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct UopMask {
bits: u32,
}
impl UopMask {
pub const NONE: UopMask = UopMask { bits: 0 };
pub const ALL: UopMask = UopMask {
bits: UOP_DEFINED_BITS,
};
#[inline]
pub const fn from_bits(bits: u32) -> Self {
Self { bits }
}
#[inline]
pub const fn raw(self) -> u32 {
self.bits
}
#[inline]
pub const fn defined_bits(self) -> u32 {
self.bits & UOP_DEFINED_BITS
}
#[inline]
pub const fn contains(self, op: UserOp) -> bool {
(self.bits & op.mask()) != 0
}
#[inline]
pub const fn is_allowed(self, op: UserOp) -> bool {
!self.contains(op)
}
#[inline]
pub const fn with(self, op: UserOp) -> Self {
Self {
bits: self.bits | op.mask(),
}
}
#[inline]
pub const fn without(self, op: UserOp) -> Self {
Self {
bits: self.bits & !op.mask(),
}
}
#[inline]
pub fn set(&mut self, op: UserOp) {
self.bits |= op.mask();
}
#[inline]
pub fn clear(&mut self, op: UserOp) {
self.bits &= !op.mask();
}
#[inline]
pub const fn is_empty(self) -> bool {
(self.bits & UOP_DEFINED_BITS) == 0
}
#[inline]
pub const fn merge_or(a: UopMask, b: UopMask, c: UopMask) -> UopMask {
UopMask {
bits: a.bits | b.bits | c.bits,
}
}
#[inline]
pub const fn count(self) -> u32 {
(self.bits & UOP_DEFINED_BITS).count_ones()
}
#[inline]
pub const fn fits_level(self, level: UopLevel) -> bool {
(self.defined_bits() & !level.cover()) == 0
}
pub fn iter(self) -> UopIter {
UopIter {
remaining: self.defined_bits(),
}
}
}
impl fmt::Display for UopMask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "UopMask(0x{:08X})", self.bits)
}
}
impl From<u32> for UopMask {
#[inline]
fn from(bits: u32) -> UopMask {
UopMask { bits }
}
}
impl From<UopMask> for u32 {
#[inline]
fn from(m: UopMask) -> u32 {
m.bits
}
}
#[derive(Debug, Clone)]
pub struct UopIter {
remaining: u32,
}
impl Iterator for UopIter {
type Item = UserOp;
fn next(&mut self) -> Option<UserOp> {
if self.remaining == 0 {
return None;
}
let bit = self.remaining.trailing_zeros() as u8;
self.remaining &= !(1u32 << bit);
UserOp::from_bit(bit)
}
}
#[inline]
pub const fn title_type_uop_mask(title_type: u8) -> UopMask {
UopMask::from_bits((title_type & 0b0000_0011) as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bit_numbers_match_spec_table_row_order() {
for (i, op) in UserOp::ALL.iter().enumerate() {
assert_eq!(op.bit() as usize, i);
assert_eq!(op.mask(), 1u32 << i);
assert_eq!(UserOp::from_bit(i as u8), Some(*op));
}
assert_eq!(UserOp::ALL.len(), UOP_BIT_COUNT as usize);
assert_eq!(UserOp::from_bit(25), None);
assert_eq!(UserOp::from_bit(31), None);
}
#[test]
fn defined_bits_cover_lower_25_only() {
assert_eq!(UOP_DEFINED_BITS, 0x01FF_FFFF);
assert_eq!(UOP_DEFINED_BITS.count_ones(), 25);
assert_eq!(UOP_DEFINED_BITS & (1 << 25), 0);
}
#[test]
fn from_bits_round_trips_raw_word() {
let raw = 0xDEAD_BEEFu32;
let m = UopMask::from_bits(raw);
assert_eq!(m.raw(), raw);
assert_eq!(m.defined_bits(), raw & UOP_DEFINED_BITS);
}
#[test]
fn contains_and_is_allowed_invert() {
let m = UopMask::NONE.with(UserOp::Stop).with(UserOp::PauseOn);
assert!(m.contains(UserOp::Stop));
assert!(m.contains(UserOp::PauseOn));
assert!(!m.contains(UserOp::Resume));
assert!(!m.is_allowed(UserOp::Stop));
assert!(m.is_allowed(UserOp::Resume));
}
#[test]
fn with_without_set_clear_match() {
let m1 = UopMask::NONE.with(UserOp::ForwardScan);
let mut m2 = UopMask::NONE;
m2.set(UserOp::ForwardScan);
assert_eq!(m1, m2);
let m3 = m1.without(UserOp::ForwardScan);
let mut m4 = m2;
m4.clear(UserOp::ForwardScan);
assert_eq!(m3, m4);
assert!(m3.is_empty());
}
#[test]
fn is_empty_ignores_reserved_bits() {
let m = UopMask::from_bits(0xFE00_0000);
assert!(m.is_empty());
assert_eq!(m.count(), 0);
}
#[test]
fn merge_or_is_plain_bitwise_or() {
let tt = title_type_uop_mask(0b11); let pgc = UopMask::NONE.with(UserOp::Stop).with(UserOp::ForwardScan);
let vobu = UopMask::NONE.with(UserOp::MenuCallRoot);
let merged = UopMask::merge_or(tt, pgc, vobu);
for op in [
UserOp::TimePlayOrSearch,
UserOp::PttPlayOrSearch,
UserOp::Stop,
UserOp::ForwardScan,
UserOp::MenuCallRoot,
] {
assert!(merged.contains(op), "merged should contain {:?}", op);
}
assert!(merged.is_allowed(UserOp::Resume));
assert!(merged.is_allowed(UserOp::AngleChange));
}
#[test]
fn merge_or_is_orthogonal_to_argument_position() {
let a = UopMask::NONE.with(UserOp::Stop);
let b = UopMask::NONE.with(UserOp::Resume);
let c = UopMask::NONE.with(UserOp::PauseOn);
let one = UopMask::merge_or(a, b, c);
let two = UopMask::merge_or(c, a, b);
let three = UopMask::merge_or(b, c, a);
assert_eq!(one, two);
assert_eq!(two, three);
}
#[test]
fn iter_walks_bits_in_ascending_order() {
let m = UopMask::NONE
.with(UserOp::VideoPresentationModeChange)
.with(UserOp::Stop)
.with(UserOp::ForwardScan);
let collected: Vec<UserOp> = m.iter().collect();
assert_eq!(
collected,
vec![
UserOp::Stop,
UserOp::ForwardScan,
UserOp::VideoPresentationModeChange,
]
);
}
#[test]
fn iter_skips_reserved_bits() {
let m = UopMask::from_bits((1u32 << 25) | (1u32 << UOP_STOP as usize));
let collected: Vec<UserOp> = m.iter().collect();
assert_eq!(collected, vec![UserOp::Stop]);
}
#[test]
fn iter_over_all_yields_every_userop_in_order() {
let all: Vec<UserOp> = UopMask::ALL.iter().collect();
assert_eq!(all.len(), 25);
for (i, op) in all.iter().enumerate() {
assert_eq!(op.bit() as usize, i);
}
}
#[test]
fn count_matches_iter_length() {
for raw in [0u32, 0x1, 0x100_0001, UOP_DEFINED_BITS, 0xFFFF_FFFF] {
let m = UopMask::from_bits(raw);
assert_eq!(m.count() as usize, m.iter().count());
}
}
#[test]
fn title_type_helper_extracts_low_two_bits_only() {
let m = title_type_uop_mask(0b1010_1011);
assert!(m.contains(UserOp::TimePlayOrSearch));
assert!(m.contains(UserOp::PttPlayOrSearch));
let m = title_type_uop_mask(0b1111_1100);
assert!(m.is_empty());
let m = title_type_uop_mask(0b1111_1101);
assert!(m.contains(UserOp::TimePlayOrSearch));
assert!(!m.contains(UserOp::PttPlayOrSearch));
let m = title_type_uop_mask(0b1111_1110);
assert!(!m.contains(UserOp::TimePlayOrSearch));
assert!(m.contains(UserOp::PttPlayOrSearch));
}
#[test]
fn title_type_helper_never_carries_bits_above_one() {
for tt in 0u8..=255 {
let m = title_type_uop_mask(tt);
assert_eq!(m.raw() & !0b11u32, 0);
assert!(m.fits_level(UopLevel::TitleSearchPointer));
}
}
#[test]
fn level_cover_matches_spec_table_columns() {
let tt = UopLevel::TitleSearchPointer.cover();
assert_eq!(tt, 0b11);
let pgc = UopLevel::ProgramChain.cover();
assert_eq!(pgc, UOP_DEFINED_BITS & !(1u32 << UOP_GO_UP));
assert_eq!(pgc.count_ones(), 24);
let vobu = UopLevel::Vobu.cover();
let blanks = (1u32 << UOP_TIME_PLAY_OR_SEARCH)
| (1u32 << UOP_PTT_PLAY_OR_SEARCH)
| (1u32 << UOP_TITLE_PLAY)
| (1u32 << UOP_BUTTON_SELECT_OR_ACTIVATE);
assert_eq!(vobu, UOP_DEFINED_BITS & !blanks);
assert_eq!(vobu.count_ones(), 21);
}
#[test]
fn user_op_applies_to_matches_level_cover_bit_for_bit() {
for op in UserOp::ALL {
for level in [
UopLevel::TitleSearchPointer,
UopLevel::ProgramChain,
UopLevel::Vobu,
] {
let from_level = (level.cover() & op.mask()) != 0;
assert_eq!(op.applies_to(level), from_level, "{:?} vs {:?}", op, level);
}
}
}
#[test]
fn fits_level_rejects_out_of_table_bits() {
let every_defined = UopMask::ALL;
assert!(!every_defined.fits_level(UopLevel::ProgramChain));
assert!(!every_defined.fits_level(UopLevel::Vobu));
assert!(!every_defined.fits_level(UopLevel::TitleSearchPointer));
let pgc_full = UopMask::from_bits(UopLevel::ProgramChain.cover());
assert!(pgc_full.fits_level(UopLevel::ProgramChain));
let vobu_ok = UopMask::NONE
.with(UserOp::Stop)
.with(UserOp::VideoPresentationModeChange);
assert!(vobu_ok.fits_level(UopLevel::Vobu));
assert!(vobu_ok.fits_level(UopLevel::ProgramChain));
assert!(!vobu_ok.fits_level(UopLevel::TitleSearchPointer));
let go_up_only = UopMask::NONE.with(UserOp::GoUp);
assert!(go_up_only.fits_level(UopLevel::Vobu));
assert!(!go_up_only.fits_level(UopLevel::ProgramChain));
}
#[test]
fn from_into_round_trip_u32() {
for raw in [0u32, 0x1, UOP_DEFINED_BITS, 0xFFFF_FFFF] {
let m: UopMask = raw.into();
let back: u32 = m.into();
assert_eq!(raw, back);
}
}
#[test]
fn display_renders_uppercase_hex() {
let m = UopMask::from_bits(0x01FF_FFFF);
let s = format!("{}", m);
assert_eq!(s, "UopMask(0x01FFFFFF)");
}
#[test]
fn merge_or_is_associative_across_three_levels() {
let a = UopMask::from_bits(0x0001_5555);
let b = UopMask::from_bits(0x0000_AAAA);
let c = UopMask::from_bits(0x0010_0001);
let direct = UopMask::merge_or(a, b, c);
let pair_then_c =
UopMask::merge_or(UopMask::from_bits(a.raw() | b.raw()), UopMask::NONE, c);
assert_eq!(direct, pair_then_c);
}
#[test]
fn spec_table_check_mark_rows_match_pgc_and_vobu_columns() {
let rows: [(UserOp, bool, bool); 25] = [
(UserOp::TimePlayOrSearch, true, false),
(UserOp::PttPlayOrSearch, true, false),
(UserOp::TitlePlay, true, false),
(UserOp::Stop, true, true),
(UserOp::GoUp, false, true),
(UserOp::TimeOrPttSearch, true, true),
(UserOp::TopPgOrPrevPgSearch, true, true),
(UserOp::NextPgSearch, true, true),
(UserOp::ForwardScan, true, true),
(UserOp::BackwardScan, true, true),
(UserOp::MenuCallTitle, true, true),
(UserOp::MenuCallRoot, true, true),
(UserOp::MenuCallSubpicture, true, true),
(UserOp::MenuCallAudio, true, true),
(UserOp::MenuCallAngle, true, true),
(UserOp::MenuCallPtt, true, true),
(UserOp::Resume, true, true),
(UserOp::ButtonSelectOrActivate, true, false),
(UserOp::StillOff, true, true),
(UserOp::PauseOn, true, true),
(UserOp::AudioStreamChange, true, true),
(UserOp::SubpictureStreamChange, true, true),
(UserOp::AngleChange, true, true),
(UserOp::KaraokeAudioMixChange, true, true),
(UserOp::VideoPresentationModeChange, true, true),
];
for (op, pgc, vobu) in rows {
assert_eq!(op.applies_to(UopLevel::ProgramChain), pgc, "PGC {:?}", op);
assert_eq!(op.applies_to(UopLevel::Vobu), vobu, "VOBU {:?}", op);
}
}
}